In this post we are going to cover how to put Claude Code into CI/CD and use it to review pull requests for a data pipeline with Claude Code GitHub Actions. We set up the workflow file, see how @claude works on a pull request, switch to automation mode with a prompt, run the same review on our own machine with the real build and data checks, and the last step is fixing the bug the review finds.
Claude Code GitHub Actions runs Claude Code inside a GitHub workflow. Someone writes @claude in a pull request comment, or the workflow runs on its own with a fixed prompt, and Claude reviews the change and runs the commands you allow. For a data engineer this is a second pair of eyes on every SQL change. Our pull request adds refunds to a daily revenue model. It looks fine, but it has a classic join bug. Let's see if the review finds it.
Part 9 of the Claude for Data Engineers series on TechBrothersIT.
- Let's create some sample data
- How Claude Code GitHub Actions works
- Step 1: Quick setup with /install-github-app
- Step 2: The workflow file, line by line
- Step 3: Automation mode with a prompt and claude_args
- Step 4: The same review, run locally
- Step 5: Fix the bug and re-run the checks
- Step 6: Headless claude -p and CI safety
- The full script
Let's create some sample data
Our project is shop-pipeline again: customers, orders and payments as CSV files, a build.py that loads them into DuckDB and builds the models, two SQL models, and a checks/run_checks.py with five data checks. The repo already has the Claude workflow in .github/workflows/claude.yml and a headless review script in ci/review.sh. Look closely at payments.csv: order 1004 has two payment rows, payment 3 and payment 12. The setup commands commit everything to main, then create the pull request branch add-refunds with the refunds change.
customer_id,name,country,signup_date
101,Ava Brooks,US,2026-08-02
102,Liam Chen,CA,2026-08-05
103,Noah Patel,US,2026-08-11
104,Mia Garcia,MX,2026-08-19
105,Zoe Martin,UK,2026-09-01
106,Omar Khan,US,2026-09-03order_id,customer_id,order_date,status,amount
1001,101,2026-10-01,completed,120.00
1002,102,2026-10-01,completed,75.50
1003,103,2026-10-01,cancelled,40.00
1004,101,2026-10-02,completed,210.00
1005,104,2026-10-02,completed,35.25
1006,105,2026-10-02,completed,99.00
1007,106,2026-10-03,completed,150.00
1008,102,2026-10-03,completed,64.75
1009,103,2026-10-03,completed,88.00
1010,104,2026-10-04,completed,45.00
1011,105,2026-10-04,completed,130.00
1012,106,2026-10-04,completed,72.00payment_id,order_id,paid_at,amount,refund_amount
1,1001,2026-10-01,120.00,0.00
2,1002,2026-10-01,75.50,0.00
3,1004,2026-10-02,150.00,30.00
4,1005,2026-10-02,35.25,0.00
5,1006,2026-10-02,99.00,0.00
6,1007,2026-10-03,150.00,0.00
7,1008,2026-10-03,64.75,10.00
8,1009,2026-10-03,88.00,0.00
9,1010,2026-10-04,45.00,0.00
10,1011,2026-10-04,130.00,0.00
11,1012,2026-10-04,72.00,0.00
12,1004,2026-10-02,60.00,0.00import pathlib
import duckdb
con = duckdb.connect("shop.duckdb")
for table in ("customers", "orders", "payments"):
con.execute(f"CREATE OR REPLACE TABLE {table} AS SELECT * FROM read_csv('data/{table}.csv', header = true)")
for f in sorted(pathlib.Path("models").glob("*.sql")):
con.execute(f"CREATE OR REPLACE VIEW {f.stem} AS " + f.read_text())
rows = con.execute(f"SELECT count(*) FROM {f.stem}").fetchone()[0]
print(f"built {f.stem:<16} {rows:>3} rows")select
o.order_date,
count(*) as orders,
sum(o.amount) as revenue
from orders o
where o.status = 'completed'
group by o.order_date
order by o.order_dateselect
o.order_id,
o.order_date,
o.amount,
c.customer_id,
c.name,
c.country
from orders o
join customers c on c.customer_id = o.customer_id"""Data checks for the shop pipeline. Exit code 1 when any check fails."""
import sys
import duckdb
con = duckdb.connect("shop.duckdb", read_only=True)
COMPLETED = "from orders where status = 'completed'"
CHECKS = {
"orders: duplicate order_id": "select order_id from orders group by order_id having count(*) > 1",
"orders: unknown customer": "select order_id from orders where customer_id not in (select customer_id from customers)",
"payments: unknown order": "select payment_id from payments where order_id not in (select order_id from orders)",
"daily_revenue: revenue": f"select m || ' vs ' || o from (select (select sum(revenue) from daily_revenue) m, (select sum(amount) {COMPLETED}) o) where m <> o",
"daily_revenue: orders": f"select m || ' vs ' || o from (select (select sum(orders) from daily_revenue) m, (select count(*) {COMPLETED}) o) where m <> o",
}
print(f"{'check':<28} result")
failed = 0
for name, sql in CHECKS.items():
bad = [str(r[0]) for r in con.execute(sql).fetchall()]
failed += bool(bad)
print(f"{name:<28} " + ("FAIL " + ", ".join(bad) if bad else "pass"))
print(f"{failed} of {len(CHECKS)} checks failed")
sys.exit(1 if failed else 0)name: Claude Code
on:
issue_comment:
types: [created]
pull_request_review_comment:
types: [created]
jobs:
claude:
if: contains(github.event.comment.body, '@claude')
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
issues: write
id-token: write
actions: read
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 1
- uses: anthropics/claude-code-action@v1
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}#!/usr/bin/env bash
# Headless review of the current branch (not run in this video: it needs a signed-in claude CLI).
git diff main...HEAD | claude -p \
"Review this diff of our data pipeline. Run the build and the data checks, list each problem and the fix." \
--allowedTools "Bash(python build.py),Bash(python checks/run_checks.py)" \
--output-format json > review.jsonshop.duckdb
__pycache__/git init -q -b main && git add -A && git commit -qm "Shop pipeline with Claude workflow"
git checkout -q -b add-refunds && cat > models/daily_revenue.sql <<'EOF'
select
o.order_date,
count(*) as orders,
sum(o.amount) as revenue,
coalesce(sum(p.refund_amount), 0) as refunds
from orders o
left join payments p on p.order_id = o.order_id
where o.status = 'completed'
group by o.order_date
order by o.order_date
EOF
git commit -qam "Add refunds to daily revenue"How Claude Code GitHub Actions works
You need three things: the Claude GitHub app on your repository, an API key or token saved as a secret, and a workflow file.
Claude Code GitHub Actions
- The Claude GitHub app on your repository
- A secret: ANTHROPIC_API_KEY or CLAUDE_CODE_OAUTH_TOKEN
- A workflow in .github/workflows/
- @claude in a comment = interactive mode
- A prompt in the workflow = automation mode
Then anyone with write access types @claude in a pull request comment, and Claude answers on the pull request. That is interactive mode. If you add a prompt to the workflow, it runs without a mention. That is automation mode. The action is built on the Claude Agent SDK.
Step 1: Quick setup with /install-github-app
The quickest setup starts inside Claude Code. Open Claude Code in your repository and type the slash command. The menu filters as you type.
/install-github-appIt installs the Claude GitHub app, saves the secret in this repository, and opens a pull request with the workflow file. You need admin rights on the repository and the GitHub command line tool signed in with gh auth login. It works for repositories on github.com.
For the secret, use ANTHROPIC_API_KEY with an API key from the Claude Console, or CLAUDE_CODE_OAUTH_TOKEN with a token from claude setup-token if you have a Pro, Max, Team or Enterprise plan.
Never put the key in the workflow file itself. It belongs in GitHub secrets.
Step 2: The workflow file, line by line
If you set it up by hand, you install the Claude GitHub app, add the secret, and copy this workflow into .github/workflows/. This is the example from the docs, and it is the same file that is in our repo.
Interactive mode: answer @claude
name: Claude Code
on:
issue_comment:
types: [created]
pull_request_review_comment:
types: [created]
jobs:
claude:
if: contains(github.event.comment.body, '@claude')
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
issues: write
id-token: write
actions: read
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 1
- uses: anthropics/claude-code-action@v1
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}The on block runs the workflow when someone writes a new issue comment, or a new review comment on a pull request. The if line means the job only starts when the comment contains @claude.
The permissions let Claude change the code and write to pull requests and issues. Then there are two steps: check out the code, and run the Claude Code action, version 1. The API key comes from GitHub secrets, through the ANTHROPIC_API_KEY secret. It is never written in the file.
Grant only the permissions you need, and review this file like code whenever it changes.
Step 3: Automation mode with a prompt and claude_args
Now automation mode. Add a prompt to the action, and it runs on its own, without an @claude mention. A common approach for a data pipeline is to tell Claude exactly what to review and which commands to run.
A prompt, max turns, allowed tools
- uses: anthropics/claude-code-action@v1
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
prompt: |
Review this pull request for data problems.
Run python build.py and python checks/run_checks.py,
then list each problem with the file and the fix.
claude_args: '--max-turns 5 --allowedTools "Bash(python build.py),Bash(python checks/run_checks.py)"'claude_args passes options to Claude Code. --max-turns 5 puts a cap on how long it can work, which also caps the cost.
This part matters: a plain prompt gets no shell tools until you allow them. So we allow exactly two commands with --allowedTools: the build and the data checks. Run it on pull request events, and every new pull request gets the same review.
By default the result goes to the workflow run log. To post it on the pull request, ask for that in the prompt and allow a tool that can post a comment, such as mcp__github_inline_comment__create_inline_comment.
Step 4: The same review, run locally
Before you trust a review in CI, run the same review on your own machine: same branch, same commands. We are on the pull request branch add-refunds, and we ask Claude to review it the way the CI reviewer would.
Review this branch like the CI reviewer: diff against main, run python build.py and python checks/run_checks.py, write review.mdFirst Claude looks at what changed: the branch name, a diff summary, then the full diff of the model.
git branch --show-current && git diff --stat main...HEADadd-refunds
models/daily_revenue.sql | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)git diff main...HEAD -- models/daily_revenue.sqldiff --git a/models/daily_revenue.sql b/models/daily_revenue.sql
index 56e2659..d628a9d 100644
--- a/models/daily_revenue.sql
+++ b/models/daily_revenue.sql
@@ -1,8 +1,10 @@
select
o.order_date,
- count(*) as orders,
- sum(o.amount) as revenue
+ count(*) as orders,
+ sum(o.amount) as revenue,
+ coalesce(sum(p.refund_amount), 0) as refunds
from orders o
+left join payments p on p.order_id = o.order_id
where o.status = 'completed'
group by o.order_date
order by o.order_dateOne file changed, models/daily_revenue.sql, with 4 insertions and 2 deletions. The change adds a left join payments and sums refund_amount as refunds. Then Claude runs the build and the data checks, exactly the two commands we allowed in the workflow.
python build.pybuilt customer_orders 12 rows
built daily_revenue 4 rowspython checks/run_checks.pycheck result
orders: duplicate order_id pass
orders: unknown customer pass
payments: unknown order pass
daily_revenue: revenue FAIL 1299.5 vs 1089.5
daily_revenue: orders FAIL 12 vs 11
2 of 5 checks failed2 of 5 checks fail, and the script exits with code 1. The model says revenue is 1299.5, but completed orders add up to 1089.5. And it counts 12 orders instead of 11. Claude looks for the cause with a query for orders that have more than one payment row.
duckdb shop.duckdb -c "SELECT order_id, count(*) AS payment_rows FROM payments GROUP BY order_id HAVING count(*) > 1"┌──────────┬──────────────┐
│ order_id │ payment_rows │
│ int64 │ int64 │
├──────────┼──────────────┤
│ 1004 │ 2 │
└──────────┴──────────────┘Order 1004 has 2 payment rows, so the join counts that order twice. The difference, 1299.5 minus 1089.5, is 210, which is exactly the amount of order 1004. That is a classic join fan-out. Claude writes the review to a file with the cause and the fix.
## Review: add-refunds
**Verdict: changes needed** (1 blocking issue)
1. **Join fan-out in `models/daily_revenue.sql`** (blocking)
Order 1004 has two payment rows (payments 3 and 12), so
`left join payments` counts it twice on 2026-10-02.
The checks fail: revenue 1299.5 vs 1089.5, orders 12 vs 11.
Fix: sum refunds per order in a CTE, then join one row per order.
2. Everything else passes: no duplicate orders, no unknown customers,
no payments for unknown orders.This is the same kind of comment you would get on the pull request: a verdict, the blocking issue with the numbers, and the fix.
Step 5: Fix the bug and re-run the checks
Now let's fix it, and prove the fix with the same checks.
Apply the fix from review.md and run the checks againClaude sums the refunds per order in a CTE first and joins that, so every order is one row again. Then it runs the build and the checks again, and queries the model.
with refunds as (
select order_id, sum(refund_amount) as refund_amount
from payments
group by order_id
)
select
o.order_date,
count(*) as orders,
sum(o.amount) as revenue,
coalesce(sum(r.refund_amount), 0) as refunds
from orders o
left join refunds r on r.order_id = o.order_id
where o.status = 'completed'
group by o.order_date
order by o.order_datepython build.py && python checks/run_checks.pybuilt customer_orders 12 rows
built daily_revenue 4 rows
check result
orders: duplicate order_id pass
orders: unknown customer pass
payments: unknown order pass
daily_revenue: revenue pass
daily_revenue: orders pass
0 of 5 checks failedduckdb shop.duckdb -c "SELECT * FROM daily_revenue"┌────────────┬────────┬─────────┬─────────┐
│ order_date │ orders │ revenue │ refunds │
│ date │ int64 │ double │ double │
├────────────┼────────┼─────────┼─────────┤
│ 2026-10-01 │ 2 │ 195.5 │ 0.0 │
│ 2026-10-02 │ 3 │ 344.25 │ 30.0 │
│ 2026-10-03 │ 3 │ 302.75 │ 10.0 │
│ 2026-10-04 │ 3 │ 247.0 │ 0.0 │
└────────────┴────────┴─────────┴─────────┘0 of 5 checks fail. The daily revenue table now adds up: 2026-10-02 has 3 orders and 344.25 in revenue, with 30.0 in refunds. Then Claude commits the fix to the same branch.
git commit -qam "Fix join fan-out in daily_revenue" && git log --oneline main..HEAD91891d9 Fix join fan-out in daily_revenue
dd5db15 Add refunds to daily revenueThe branch now has two commits on top of main: dd5db15 with the refunds change and 91891d9 with the fix. Push it, mention @claude on the pull request again, and Claude reviews the new code.
Put these data checks in your repository, so the same commands run on your laptop and in CI. Then a review is something you can repeat, not an opinion.
Step 6: Headless claude -p and CI safety
You can also run a review from any script or CI system with claude -p. That is headless mode, and it is what ci/review.sh from the starting files does. It pipes in the diff, gives the prompt, and allows only the build and the checks. In current versions a -p run starts in manual mode, so anything you did not allow is denied.
--output-format json gives you a result you can parse, and the exit code is 0 on success. We did not run this script here, because it needs a signed-in claude CLI.
For CI, the docs recommend --bare. It skips hooks, skills, MCP servers and CLAUDE.md, and it needs an API key.
And keep it safe. Use GitHub secrets for keys, grant minimal workflow permissions, cap turns and job time, and always review what Claude changes before you merge.
That is Claude Code in CI: the workflow from the docs, @claude on a pull request, automation mode with a prompt, and the same review run locally with real checks that caught a real join bug. In the next part, the last one in this series, we build an end to end data project with Claude Code, from a raw CSV file to a dashboard.
Key points to remember
- Claude GitHub app + secret + workflow
- @claude = interactive mode
- prompt + claude_args = automation mode
- --allowedTools: only the commands it needs
- Same review locally: diff, build, checks
- Headless claude -p (--bare in CI)
The full script
This bash script writes every file Claude wrote, in the same order, and runs every command, so you can reproduce the results without Claude.
Show the full script (every file and every command, in order)
#!/usr/bin/env bash
# Claude for Data Engineers #9: Claude Code in CI/CD: Review Data Pipeline Pull Requests with GitHub Actions
# Rebuilds the demo project without Claude: writes every file Claude wrote, in the same order,
# and runs every command from the video. Run it in an empty folder.
# Tools used in the video: Python 3.13, DuckDB 2.0.0, pandas 3.0.5, PySpark 4.3.0 (local mode, Java 21),
# dbt-core 1.12.6 with dbt-duckdb 1.11.0, PostgreSQL 16. Install the ones this project uses first.
mkdir -p shop-pipeline && cd shop-pipeline
# ---------- starting files ----------
mkdir -p data
cat > data/customers.csv <<'CLAUDE_DE_EOF'
customer_id,name,country,signup_date
101,Ava Brooks,US,2026-08-02
102,Liam Chen,CA,2026-08-05
103,Noah Patel,US,2026-08-11
104,Mia Garcia,MX,2026-08-19
105,Zoe Martin,UK,2026-09-01
106,Omar Khan,US,2026-09-03
CLAUDE_DE_EOF
mkdir -p data
cat > data/orders.csv <<'CLAUDE_DE_EOF'
order_id,customer_id,order_date,status,amount
1001,101,2026-10-01,completed,120.00
1002,102,2026-10-01,completed,75.50
1003,103,2026-10-01,cancelled,40.00
1004,101,2026-10-02,completed,210.00
1005,104,2026-10-02,completed,35.25
1006,105,2026-10-02,completed,99.00
1007,106,2026-10-03,completed,150.00
1008,102,2026-10-03,completed,64.75
1009,103,2026-10-03,completed,88.00
1010,104,2026-10-04,completed,45.00
1011,105,2026-10-04,completed,130.00
1012,106,2026-10-04,completed,72.00
CLAUDE_DE_EOF
mkdir -p data
cat > data/payments.csv <<'CLAUDE_DE_EOF'
payment_id,order_id,paid_at,amount,refund_amount
1,1001,2026-10-01,120.00,0.00
2,1002,2026-10-01,75.50,0.00
3,1004,2026-10-02,150.00,30.00
4,1005,2026-10-02,35.25,0.00
5,1006,2026-10-02,99.00,0.00
6,1007,2026-10-03,150.00,0.00
7,1008,2026-10-03,64.75,10.00
8,1009,2026-10-03,88.00,0.00
9,1010,2026-10-04,45.00,0.00
10,1011,2026-10-04,130.00,0.00
11,1012,2026-10-04,72.00,0.00
12,1004,2026-10-02,60.00,0.00
CLAUDE_DE_EOF
cat > build.py <<'CLAUDE_DE_EOF'
import pathlib
import duckdb
con = duckdb.connect("shop.duckdb")
for table in ("customers", "orders", "payments"):
con.execute(f"CREATE OR REPLACE TABLE {table} AS SELECT * FROM read_csv('data/{table}.csv', header = true)")
for f in sorted(pathlib.Path("models").glob("*.sql")):
con.execute(f"CREATE OR REPLACE VIEW {f.stem} AS " + f.read_text())
rows = con.execute(f"SELECT count(*) FROM {f.stem}").fetchone()[0]
print(f"built {f.stem:<16} {rows:>3} rows")
CLAUDE_DE_EOF
mkdir -p models
cat > models/daily_revenue.sql <<'CLAUDE_DE_EOF'
select
o.order_date,
count(*) as orders,
sum(o.amount) as revenue
from orders o
where o.status = 'completed'
group by o.order_date
order by o.order_date
CLAUDE_DE_EOF
mkdir -p models
cat > models/customer_orders.sql <<'CLAUDE_DE_EOF'
select
o.order_id,
o.order_date,
o.amount,
c.customer_id,
c.name,
c.country
from orders o
join customers c on c.customer_id = o.customer_id
CLAUDE_DE_EOF
mkdir -p checks
cat > checks/run_checks.py <<'CLAUDE_DE_EOF'
"""Data checks for the shop pipeline. Exit code 1 when any check fails."""
import sys
import duckdb
con = duckdb.connect("shop.duckdb", read_only=True)
COMPLETED = "from orders where status = 'completed'"
CHECKS = {
"orders: duplicate order_id": "select order_id from orders group by order_id having count(*) > 1",
"orders: unknown customer": "select order_id from orders where customer_id not in (select customer_id from customers)",
"payments: unknown order": "select payment_id from payments where order_id not in (select order_id from orders)",
"daily_revenue: revenue": f"select m || ' vs ' || o from (select (select sum(revenue) from daily_revenue) m, (select sum(amount) {COMPLETED}) o) where m <> o",
"daily_revenue: orders": f"select m || ' vs ' || o from (select (select sum(orders) from daily_revenue) m, (select count(*) {COMPLETED}) o) where m <> o",
}
print(f"{'check':<28} result")
failed = 0
for name, sql in CHECKS.items():
bad = [str(r[0]) for r in con.execute(sql).fetchall()]
failed += bool(bad)
print(f"{name:<28} " + ("FAIL " + ", ".join(bad) if bad else "pass"))
print(f"{failed} of {len(CHECKS)} checks failed")
sys.exit(1 if failed else 0)
CLAUDE_DE_EOF
mkdir -p .github/workflows
cat > .github/workflows/claude.yml <<'CLAUDE_DE_EOF'
name: Claude Code
on:
issue_comment:
types: [created]
pull_request_review_comment:
types: [created]
jobs:
claude:
if: contains(github.event.comment.body, '@claude')
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
issues: write
id-token: write
actions: read
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 1
- uses: anthropics/claude-code-action@v1
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
CLAUDE_DE_EOF
mkdir -p ci
cat > ci/review.sh <<'CLAUDE_DE_EOF'
#!/usr/bin/env bash
# Headless review of the current branch (not run in this video: it needs a signed-in claude CLI).
git diff main...HEAD | claude -p \
"Review this diff of our data pipeline. Run the build and the data checks, list each problem and the fix." \
--allowedTools "Bash(python build.py),Bash(python checks/run_checks.py)" \
--output-format json > review.json
CLAUDE_DE_EOF
cat > .gitignore <<'CLAUDE_DE_EOF'
shop.duckdb
__pycache__/
CLAUDE_DE_EOF
# ---------- setup ----------
git init -q -b main && git add -A && git commit -qm "Shop pipeline with Claude workflow"
git checkout -q -b add-refunds && cat > models/daily_revenue.sql <<'EOF'
select
o.order_date,
count(*) as orders,
sum(o.amount) as revenue,
coalesce(sum(p.refund_amount), 0) as refunds
from orders o
left join payments p on p.order_id = o.order_id
where o.status = 'completed'
group by o.order_date
order by o.order_date
EOF
git commit -qam "Add refunds to daily revenue"
# ---------- step 1: What the pull request changes: the diff against main ----------
git branch --show-current && git diff --stat main...HEAD
git diff main...HEAD -- models/daily_revenue.sql
# ---------- step 2: Run the build and the data checks, like the reviewer in CI would ----------
python build.py
python checks/run_checks.py # expected to fail
# ---------- step 3: Find the cause: order 1004 has two payment rows ----------
duckdb shop.duckdb -c "SELECT order_id, count(*) AS payment_rows FROM payments GROUP BY order_id HAVING count(*) > 1"
# ---------- step 4: The review Claude writes ----------
cat > review.md <<'CLAUDE_DE_EOF'
## Review: add-refunds
**Verdict: changes needed** (1 blocking issue)
1. **Join fan-out in `models/daily_revenue.sql`** (blocking)
Order 1004 has two payment rows (payments 3 and 12), so
`left join payments` counts it twice on 2026-10-02.
The checks fail: revenue 1299.5 vs 1089.5, orders 12 vs 11.
Fix: sum refunds per order in a CTE, then join one row per order.
2. Everything else passes: no duplicate orders, no unknown customers,
no payments for unknown orders.
CLAUDE_DE_EOF
# ---------- step 5: Fix: aggregate refunds per order before the join, then run the checks again ----------
mkdir -p models
cat > models/daily_revenue.sql <<'CLAUDE_DE_EOF'
with refunds as (
select order_id, sum(refund_amount) as refund_amount
from payments
group by order_id
)
select
o.order_date,
count(*) as orders,
sum(o.amount) as revenue,
coalesce(sum(r.refund_amount), 0) as refunds
from orders o
left join refunds r on r.order_id = o.order_id
where o.status = 'completed'
group by o.order_date
order by o.order_date
CLAUDE_DE_EOF
python build.py && python checks/run_checks.py
duckdb shop.duckdb -c "SELECT * FROM daily_revenue"
# ---------- step 6: Commit the fix to the pull request branch ----------
git commit -qam "Fix join fan-out in daily_revenue" && git log --oneline main..HEAD
Part 10: End-to-End Data Project with Claude Code: Raw CSV to Dashboard
The project and its data are invented for teaching. Every file on this page is the real file from the demo project, and every output is the real output of the command above it, captured when the video was made (each step ran twice in a fresh folder and the outputs were compared). The Claude screens in the video are a simplified recreation for teaching. Always review what Claude changes and runs. Claude plans, features and limits are current as of November 2026; check the official Claude and Claude Code documentation before you rely on them. Part of the Claude for Data Engineers series on TechBrothersIT.



No comments:
Post a Comment
Note: Only a member of this blog may post a comment.