The inner loop is the cycle an engineer runs dozens of times per hour: edit, run, inspect, fix, repeat. Nothing else affects productivity more. A 5-second loop produces shipped features. A 5-minute loop produces context-switching and Slack scrolling.
This guide walks through the inner loop for each stack a data creator works in, with concrete keystrokes and command lines. The target is seconds per loop, never minutes.
The principle: never leave the editor
The moment you tab out of VS Code, the loop breaks. Attention fragments, typos multiply, and the cost of each iteration doubles. Every stack in this guide has been configured so the entire loop — compile, run, inspect, test — happens inside VS Code.
Important
If your loop requires the Databricks workspace UI, Snowsight, or Airflow's web UI to observe results, the loop is broken. Fix that first. Every tool below includes an inline inspection path.
Python (local, pure)
The baseline. A unit-testable function, no cloud dependencies.
- Edit the
.pyfile. Pylance autocompletes as you type, Ruff formats on save. - Press
Cmd+;to run all tests, orCmd+; Cmd+Fto run the current file's tests. - Read the result in the Test Explorer panel or the integrated terminal. Failures hyperlink back to the failing line.
- If a test fails, click the ▷ next to the test in the gutter, pick Debug Test. Breakpoint hits; you inspect.
- Fix, save, repeat.
A typical loop: 2–5 seconds per iteration.
Tip
Install pytest-xdist and run pytest -n auto. Parallelizing tests across cores collapses even a hundred-test suite to sub-second runs.
PySpark via Databricks Connect
The pattern for any work against real Spark: local driver, remote cluster.
Setup once
- Install
databricks-connectmatching your cluster's DBR version:uv add databricks-connect==15.4.*. - Authenticate the Databricks extension via OAuth.
- Attach a cluster (or serverless).
The loop
- Edit the
.pyfile. Write a PySpark expression:df.groupBy("region").count().show(). - Click Run on Databricks → Debug current file with Databricks Connect, or press the run shortcut bound in
keybindings.json. - Code runs locally until it hits a DataFrame operation; the operation executes on the remote cluster; results stream back to the integrated terminal.
- Inspect
dfin the Variables pane if a breakpoint is set. The inspector shows schema, row count, and a sampled preview. - Fix, save, repeat.
A typical loop: 5–10 seconds for a small transform. Longer if the cluster is auto-starting — keep it warm during active work.
Warning
The cluster bills while it is running. Set auto-terminate to 30 minutes on interactive clusters. Shared serverless compute is even better for interactive work — it auto-sleeps without bill shock.
When the loop is slow
- Cold cluster start. 3–5 minutes. Wake the cluster before your first edit of the day.
- Network latency. Each DataFrame operation round-trips. Chain operations before calling
.show()or.collect()— a singledf.filter(...).groupBy(...).count()is one round trip, not three. collect()on large results. The full result DataFrame streams to the laptop. Useshow()orlimit().collect()for inspection, notcollect().
dbt
The dbt inner loop has two phases: edit-and-inspect (local) and build-and-test (remote-compute-on-dev-schema).
Setup once
- Install dbt Fusion LSP (
dbtLabsInc.dbt) or dbt Power User (innoverio.vscode-dbt-power-user). - Configure
profiles.ymlwith a dev target that writes to your personal dev schema. dbt depsfrom the integrated terminal.
The edit-and-inspect loop
- Open a model file.
- As you edit, the LSP (or Power User) shows compiled SQL in a side panel live. CTE previews render inline.
- Hover any
{{ ref('...') }}to see the resolved table name. - Save.
dbt parseruns in the background; syntax and DAG errors surface in the Problems pane.
This loop is sub-second — you catch half of dbt bugs before you ever run dbt build.
The build-and-test loop
- In the integrated terminal:
dbt build --select model_name. - Build and tests run on the warehouse against your dev schema. Typical completion: 10–30 seconds for a small model.
- Failures report as "FAIL 1 unique_fct_orders_order_id" with a click-through URL to the query.
- For a faster iteration,
dbt build --select state:modified+ --defer --state target/. This runs only what changed and everything downstream that depends on it, unioned with prod state for upstreams.
Tip
dbt build --select +my_model is read as "my_model and everything upstream." --select my_model+ is "my_model and everything downstream." Memorize the difference; the + position encodes the DAG direction.
When the loop is slow
- Warehouse cold-start (Databricks SQL, Snowflake auto-suspend). Keep the warehouse warm during active work.
- Unnecessary upstream recompiles. Use
--defer+--stateso only the modified model and its true descendants recompute. - Slow tests. Profile
dbt_expectationstests; some (likeexpect_row_values_to_be_in_setagainst a large reference table) are surprisingly expensive.
Airflow
Local Airflow iterates fast. Production Airflow is for production.
Setup once
- Install Astro CLI on PATH.
- In the project directory:
astro dev init(or clone an existing Astro project). astro dev start— this spins up a local Airflow stack (scheduler, triggerer, webserver, Postgres) in Docker.
The DAG-author loop
- Edit a DAG
.pyfile. Hot reload picks up the change within 30 seconds. - Open the Airflow UI (
localhost:8080) in a browser tab only when you need to trigger a run manually. For pure DAG-render checks, the file tree in VS Code andastro dev run dags listare enough. - Run a test execution headlessly from the terminal:
astro dev run dags test my_dag 2026-04-20. This runs the entire DAG synchronously with full stack traces. - Read the logs in the terminal, not the UI.
A typical loop: 10–20 seconds for a small DAG; longer for DAGs with real external work.
Important
dags test runs tasks synchronously, bypassing the scheduler. It is the fastest way to check a DAG end-to-end. Do not use it for testing scheduler behavior (retries, pool semantics, SLA), only for logic.
The task-callable loop
Task callables are pure Python. Test them the same way as any Python function:
- Extract the callable from the DAG module.
- Write a pytest unit test.
- Run via Test Explorer. Seconds per iteration.
Tip
If you find yourself using astro dev run dags test to validate task-level logic, you are treating Airflow as the test harness. Move the logic to a testable function and unit-test it directly. Airflow tests orchestration, not business logic.
Notebooks (locally-authored, remotely-executed)
Some data exploration still warrants notebooks. VS Code's notebook experience is competitive:
- Open a
.ipynbin VS Code. - Select the remote Databricks kernel (the Databricks extension exposes a kernel per configured cluster).
- Run cells with
Shift+Enter. Each cell executes on the remote cluster. - Inspect outputs inline, including DataFrames with pagination.
Or use # %% cell markers in a plain .py file for "notebook mode without .ipynb":
# %%
df = spark.table("main.silver.events")
df.count()
# %%
df.groupBy("region").count().show()
# %% cells run independently; state persists across cells in the same kernel session. The files live in Git cleanly (unlike .ipynb).
Note
Prefer # %% over .ipynb for anything that will live in the repo. Notebooks are fine for exploration; their diffs are toxic for review.
Multi-stack projects
A real data product mixes stacks: dbt models, Airflow DAGs that orchestrate them, Python fixtures that feed both, PySpark jobs for the heavy bronze→silver lift. The inner loop across stacks needs discipline:
- Split panes in the integrated terminal. One pane per tool:
dbt,astro,pythonREPL. Name them (right-click → Rename) so you do not lose track. - Tasks in
.vscode/tasks.jsonfor the commands you run dozens of times a day. Bind them to keyboard shortcuts. See the commands reference. - One workspace per project. Multi-root workspaces help when two repos need coordinated edits; do not use them to paper over a monorepo issue that wants a monorepo solution.
The no-browser rule
Every browser tab you open during the inner loop costs a 30-second attention tax. Enforce a rule: no browser tab until the loop succeeds.
- Compile: inline in the dbt LSP.
- Logs: integrated terminal.
- Query results: debug console or
df.show(). - Diffs: Source Control panel.
- PR review: GitHub Pull Requests extension.
The only browser tab that earns its keep is the one you open at the end to read a dashboard or confirm a DAG ran in production. Every other one is a leak.
Typical loop times: the target
A well-configured setup hits these:
| Loop | Typical iteration |
|---|---|
| Unit test (pure Python) | 1–3 s |
| Unit test (PySpark with ChispaFixture and local Spark) | 3–5 s |
dbt build --select <model> (small model, warm warehouse) | 10–30 s |
dbt build --select state:modified+ --defer | 15–60 s |
| Databricks Connect run of a small transform | 5–10 s |
astro dev run dags test for a small DAG | 10–20 s |
| PR review cycle (open PR, diff, approve) | 1–2 min for a small PR |
If your real loop times are 3× these, the setup is wrong, not the work.
Anti-patterns
- Running
dbt buildwith no--select. Every iteration compiles every model. Loop time explodes. - Running
pytestwith no filter. Same problem, in Python. Usepytest -k <expr>or the Test Explorer. - Using the Databricks workspace UI to iterate. The UI's save-and-run cycle is slower than the Connect + debugger path, and diffs do not land in Git.
- Hot-reloading Airflow DAGs by restarting the whole stack.
astro dev starthot-reloads automatically. No restart needed. - Inspecting DataFrames by running
collect(). Streams the whole result.show()orlimit().collect()almost always works.
See also
- Debugging — the launch configs that make these loops fast.
- Workspace standards — how the paved path configures this.
- The extension pack reference — the extensions that power each loop.
- Commands reference — the keystrokes and Tasks that shave seconds off each iteration.