Most Looker estates reach a point where one project is no longer enough. Finance wants its own release cadence, the product team wants to iterate daily, and a central data team owns the conformed dimensions everybody joins to. Splitting into several Looker projects solves the governance problem and immediately creates a duplication problem: the same dimension_group on created_at, the same fiscal-year logic, the same customers view, copy-pasted three times and already drifting.
LookML has a proper answer to this: project imports. One project is published as a shared library, other projects declare it as a dependency in manifest.lkml, and files are included across the project boundary with a //project_name prefix. Combined with constants and refinements, it gives you a real internal LookML package.
This tutorial walks through the setup, the difference between local and remote imports, how to pin a version so a shared change cannot break downstream dashboards overnight, and where teams usually go wrong.
The shape of the solution
A typical layout after this refactor:
analytics_core— no Explores anybody uses directly. It holds base views, conformed dimensions, date logic, naming constants, and (optionally) a set of base Explores meant to be extended.finance,product,marketing— thin consumer projects. They importanalytics_core, refine or extend what they need, and own their own model files, Explores, and dashboards.
The consumer projects stay small, and a fix to fiscal-calendar logic lands in one place.
Step 1: make the shared project importable
In the shared project, add a manifest.lkml file at the project root that declares it exportable:
# analytics_core/manifest.lkml
project_name: "analytics_core"
# Which files other projects are allowed to include.
# "*" exports everything; a list is better hygiene.
localization_settings: {
default_locale: en
}
Export permissions are controlled per file set with constant and include rules on the consumer side, but the important declaration on the producer side is project_name. It is the name downstream projects will use in their // include paths, and it does not have to match the Git repository name — pick something stable, because renaming it later breaks every consumer include.
One rule worth adopting on day one: the shared project should be boring. No connection-specific sql_table_name hardcoding, no Explores that assume a particular model, no user attributes that only exist in one instance. Everything environment-specific belongs in the consumer project.
Step 2: declare the dependency in the consumer project
In the consumer project's manifest.lkml:
# finance/manifest.lkml
project_name: "finance"
# Same Looker instance, same Git host, managed by Looker:
local_dependency: {
project: "analytics_core"
}
A local dependency points at another project on the same Looker instance. Looker resolves it directly, and changes in the shared project are visible to the consumer as soon as they are deployed to production. That immediacy is convenient in development and dangerous in production — see pinning below.
A remote dependency pulls the shared LookML from a Git URL instead, which is what you want when the shared library lives outside this instance (a different Looker deployment, an open-source block, or a repo you publish for clients):
# finance/manifest.lkml
project_name: "finance"
remote_dependency: analytics_core {
url: "https://github.com/acme-data/analytics_core"
ref: "v2.4.1"
}
After saving the manifest, open the project's IDE and run Update Dependencies (the IDE prompts you). Looker fetches the remote repo at that ref and caches it under imported_projects/. Nothing updates until you change the ref and update dependencies again — which is exactly the property you want.
local vs remote: how to choose
local_dependency | remote_dependency | |
|---|---|---|
| Source | Another project on this instance | Any Git URL |
| Versioning | Follows the shared project's production state | Pinned to ref (tag, branch, or SHA) |
| Update trigger | Shared project deploy | Explicit Update Dependencies |
| Best for | One team, tight iteration | Multi-team, multi-instance, client deliverables |
A pattern that works well in practice: local_dependency while a shared model is being actively co-developed, then switch to remote_dependency with a tagged ref once downstream dashboards start mattering.
Step 3: include files across the boundary
Inside the consumer project, an imported file is addressed with a leading // and the project name:
# finance/views/invoices.view.lkml
include: "//analytics_core/views/customers.view.lkml"
include: "//analytics_core/views/date_helpers.view.lkml"
Those views are now usable in the consumer project's Explores as if they were local:
# finance/models/finance.model.lkml
connection: "warehouse_prod"
include: "//analytics_core/views/*.view.lkml"
include: "/views/*.view.lkml"
include: "/dashboards/*.dashboard.lookml"
explore: invoices {
join: customers {
type: left_outer
relationship: many_to_one
sql_on: ${invoices.customer_id} = ${customers.id} ;;
}
}
Note the include order: imported files first, local files second. Later includes win when names collide, so local overrides sit last on purpose.
Step 4: customize without forking
The whole point of importing is that you do not edit the shared file. Two safe ways to adapt it locally:
Refine it — add or override fields on the imported view from the consumer project:
# finance/views/customers_finance.view.lkml
include: "//analytics_core/views/customers.view.lkml"
view: +customers {
# Finance labels the same field differently
dimension: name {
label: "Legal Entity Name"
}
measure: arr {
type: sum
sql: ${TABLE}.annual_recurring_revenue ;;
value_format_name: usd_0
}
}
Extend it — create a new, separate view when finance needs a genuinely different object rather than a tweak:
view: customers_finance {
extends: [customers]
# ...finance-only fields, original view untouched
}
Refine when everyone should see the change in this project. Extend when you need two coexisting variants. (Both behave exactly as they do within a single project — the import boundary changes nothing about the semantics.)
Step 5: use constants for the things that differ
Constants are the mechanism that keeps a shared project environment-agnostic. Declare defaults in the shared manifest and override them per consumer:
# analytics_core/manifest.lkml
project_name: "analytics_core"
constant: warehouse_schema {
value: "analytics"
export: override_optional
}
constant: company_label {
value: "Acme"
export: override_required
}
# finance/manifest.lkml
project_name: "finance"
local_dependency: {
project: "analytics_core"
override_constant: warehouse_schema { value: "analytics_finance" }
override_constant: company_label { value: "Acme Financial Services" }
}
And in the shared LookML:
view: customers {
sql_table_name: @{warehouse_schema}.customers ;;
label: "@{company_label} Customers"
}
The export setting is the contract:
export: none(default) — the constant is private to the shared project.export: override_optional— consumers may override it; the shared default applies otherwise.export: override_required— consumers must supply a value, and the project will not validate until they do. Use this for anything where a silent default would be wrong (schema names, region filters, entity labels).
override_required is the underrated one. It converts "someone forgot to configure the import" from a wrong number on a dashboard into a LookML validation error.
Versioning the shared project
Once more than one team consumes analytics_core, treat it like software:
- Tag releases.
v2.4.1on the shared repo, referenced byrefin each consumer. Consumers upgrade deliberately. - Never rename or delete an exported field in a patch release. Deprecate first: keep the field, add
hidden: yesplus adescriptionpointing at the replacement, remove it in the next major tag. - Run the consumers' validators before tagging. LookML validation only sees one project at a time, so a shared change can validate clean and still break
finance. In CI, check out each consumer at its currentref, point it at the candidate commit, and validate. Content Validator afterwards catches broken Looks and dashboards. - Write a changelog file in the shared repo. The consumers' developers are your users;
git logis not a release note.
Gotchas we see in audits
- A shared project with Explores that everybody imports. Explores tie a model to a connection and to specific joins. Share views, conformed dimensions, constants, and at most base Explores intended for
extends. Ship the real Explores from the consumer project. include: "//core/**/*.view.lkml"everywhere. Importing the whole shared project into every model bloats the model and makes field-name collisions likely. Import what you use.- Local dependency in production with no pin. A deploy to the shared project instantly changes every consumer. If that scares you, it should: move to
remote_dependencywith a tag. - Circular dependencies.
coreimportingfinancewhilefinanceimportscorewill not resolve. The dependency graph must stay a tree; if two projects need each other's logic, that logic belongs in the shared project. - Forgetting Update Dependencies after changing
ref. The IDE keeps serving the cached copy, and developers spend an afternoon wondering why the new field is missing. - Constants used for things that should be user attributes. A constant is resolved at LookML-compile time, so it is the same for every user. Anything that varies per viewer (region, tenant, entity access) is a user attribute, not a constant.
- Duplicated dbt-generated views. If dbt generates base views, generate them into the shared project once and import them, rather than into each project.
Migration path for an existing single-project estate
You do not need a big-bang refactor:
- Create the shared project with a
manifest.lkmland one thing in it — usually the date/fiscal-calendar view or a constants file. - Add
local_dependencyto a single consumer project and swap its local copy for the import. Validate, run Content Validator, deploy. - Move conformed dimensions next (customer, product, account), one view per pull request, refining locally where a team's labels differ.
- When two or more teams depend on it, tag the shared project and switch consumers to
remote_dependencywith pinned refs. - Only then split the remaining monolith by domain.
Each step is independently reversible, which matters when dashboards are in daily use.
Wrap-up
Project imports turn a sprawl of near-duplicate LookML into a small shared core plus thin, team-owned consumer projects. manifest.lkml declares the dependency, //project/path includes cross the boundary, refinements and extends customize without forking, constants with override_required make configuration explicit, and pinned refs mean a central change can never surprise a downstream dashboard.
If your Looker estate has three copies of the same date logic and nobody is sure which one is authoritative, that is the signal to start. Vistelio's senior Looker developers do this refactor regularly — including the CI setup that validates every consumer project before a shared release is tagged. Get in touch if you want a second pair of eyes on the dependency graph before you commit to it.