Labs4Change

LookML Best Practices: The Folder Structure That Scales

How to organize your Looker project with auto-generated views, refinements, and a clean folder structure that doesn't fall apart at 500 views.

Most Looker projects start clean. A handful of views, a couple of explores, everything in one folder. Then reality hits — new stakeholders, new data sources, a second developer, a third model — and suddenly the project is a maze of duplicated SQL, mystery fields, and explores nobody can explain.

After training over 16,000 analytics engineers and auditing hundreds of Looker instances, we see the same structural mistakes over and over. This guide covers the foundational pattern that separates projects that scale from ones that collapse under their own weight: how you organize your files and where your code lives.

The Problem With Most Looker Projects

Here's what typically happens:

  1. Someone generates views from the database
  2. They start adding measures, labels, and descriptions directly into those auto-generated files
  3. The schema changes upstream — a column gets renamed, a new table appears
  4. They need to regenerate the view, but can't — because all their business logic is mixed in
  5. They start copy-pasting, duplicating logic, and the project spirals

The fix is simple in principle: never modify auto-generated views directly. But you need a project structure that enforces this cleanly.

The Project Structure

your_project/
├── views/
│   ├── _autogen/              # Auto-generated views (read-only)
│   │   ├── orders.view.lkml
│   │   └── users.view.lkml
│   ├── _refined/              # Business logic via refinements
│   │   ├── orders_r.view.lkml
│   │   └── users_r.view.lkml
│   └── derived_tables/        # NDTs, SQL DTs, PDTs
│       └── customer_ltv.view.lkml
├── explores/                  # Explore definitions (not in view files)
│   └── ecommerce.explore.lkml
├── models/
│   └── ecommerce.model.lkml
├── tests/                     # Data tests
│   └── order_tests.lkml
└── dashboards/                # LookML dashboards (optional)

Every folder has a single responsibility. No ambiguity about where code goes.

Auto-Generated Views: The Read-Only Foundation

When you create a view from a database table — via Create View from Table or by generating a project from a connection — Looker produces a clean one-to-one mapping of your schema. Every column becomes a dimension with a ${TABLE}.column_name reference.

This layer is sacred. Treat it as read-only.

# File: views/_autogen/orders.view.lkml
# Auto-generated — do not modify directly

view: orders {
  sql_table_name: `project.dataset.orders` ;;

  dimension: order_id {
    type: number
    sql: ${TABLE}.order_id ;;
  }

  dimension: status {
    type: string
    sql: ${TABLE}.status ;;
  }

  dimension_group: created {
    type: time
    timeframes: [raw, date, week, month, quarter, year]
    sql: ${TABLE}.created_at ;;
  }

  dimension: amount {
    type: number
    sql: ${TABLE}.amount ;;
  }
}

No labels. No descriptions. No measures. No business logic. Just the raw schema mapping.

When your upstream table changes, you regenerate this file and nothing breaks — because all your business logic lives in the next layer.

Refinements: Where Business Logic Lives

Refinements use the + prefix to modify any existing view without touching the original file:

# File: views/_refined/orders_r.view.lkml
include: "/views/_autogen/orders.view.lkml"

view: +orders {
  # Add primary key
  dimension: order_id {
    primary_key: yes
    description: "Unique identifier for each order."
  }

  # Hide raw fields that don't belong in the field picker
  dimension: amount {
    hidden: yes
  }

  # Add business logic
  measure: total_revenue {
    type: sum
    sql: ${amount} ;;
    description: "Sum of all order amounts in USD."
    value_format_name: usd
  }

  measure: order_count {
    type: count
    description: "Total number of orders."
    drill_fields: [order_id, created_date, status, total_revenue]
  }
}

Looker merges the refinement into the original at compile time. You get one orders view with all the auto-generated fields plus your measures, labels, and descriptions — but the code is cleanly separated across files.

Naming Convention

File typeNamingExample
Auto-generated viewsMatch database tableorders.view.lkml
Refined viewsAppend _rorders_r.view.lkml
View object namesAutogen matches file name, refined uses +view: +orders { }

Keep the view file name and the view object name identical. When you have dozens of views, being able to find orders.view.lkml by searching for orders (instead of guessing some alias) saves real time.

The Model File: Wiring It Together

Your model file pulls everything in via includes:

# File: models/ecommerce.model.lkml
connection: "your_connection"

include: "/views/_autogen/*.view.lkml"
include: "/views/_refined/*.view.lkml"
include: "/views/derived_tables/*.view.lkml"
include: "/explores/*.explore.lkml"

For larger projects, use the layers pattern — separate include files that act like feature flags:

# _base.layer.lkml — auto-generated views + basic explores
include: "/views/_autogen/*.view.lkml"
include: "/explores/*.explore.lkml"

# _logical.layer.lkml — all business logic refinements
include: "/_base.layer.lkml"
include: "/views/_refined/*.view.lkml"

Then in your model, toggle layers by commenting/uncommenting:

connection: "your_connection"
include: "/_logical.layer.lkml"
# include: "/_experimental.layer.lkml"   # WIP feature

Need to roll back a set of changes? Comment out the include. Working on something experimental? Keep it in its own layer until it's ready.

Explores: Keep Them Separate

Explores should live in their own folder, not inside view files. Group them by business domain:

# File: explores/ecommerce.explore.lkml
include: "/views/_autogen/orders.view.lkml"
include: "/views/_autogen/users.view.lkml"
include: "/views/_refined/orders_r.view.lkml"
include: "/views/_refined/users_r.view.lkml"

explore: orders {
  label: "Customer Orders"
  description: "Analyze order patterns, revenue, and customer behavior.
    Key filters: order date, status, region."

  join: users {
    type: left_outer
    relationship: many_to_one
    sql_on: ${orders.user_id} = ${users.id} ;;
  }
}

This prevents merge conflicts (multiple developers editing the same file) and makes it clear which explores exist without scanning through every view file.

The Rules

  1. One view per file. File name matches view name.
  2. Auto-generated views are read-only. Only ${TABLE}.column_name syntax. No labels, no descriptions, no measures.
  3. All business logic goes in refinements. Primary keys, descriptions, measures, labels — all in _refined/ files.
  4. Explores live in their own folder. Not inside view files.
  5. Same structure across all projects. If you have multiple Looker projects, keep the folder convention consistent.
  6. Every view has a primary key. Defined in the refinement. No exceptions — Looker's symmetric aggregates depend on it.

This Is Just the Foundation

Folder structure is step one. The projects that truly scale also nail five other patterns:

  • The substitution operator — using ${TABLE}.col vs ${field} vs ${view.field} correctly (most projects get this wrong)
  • Derived tables — when to use NDTs vs SQL DTs vs PDTs, and the always_filter pitfall that silently corrupts your data
  • Descriptions — the most underused LookML parameter, and the one that makes or breaks AI features
  • AI context — how to write LookML metadata that makes Gemini's natural language queries actually work
  • Refinements deep dive — the layers pattern, extends vs refinements, and controlling access with model sets

We've packaged all six patterns into a complete guide, plus a LookML Best Practices AI Skill — a drop-in file you can add to your AI coding agent (Claude Code, Cursor, Copilot) so it follows these patterns every time it writes LookML for you.

Get the Complete LookML Best Practices Guide + AI Skill

The full guide covers all 6 patterns with production code examples, plus a ready-to-use AI agent skill file that enforces these best practices every time your AI writes LookML. Enter your email and we'll send it over.


Labs4Change has trained over 16,000 analytics engineers on Looker and LookML. If your Looker instance needs an architecture audit or your team needs hands-on training, book a free strategy call.