---
title: "Imports"
description: Learn how to use the @import decorator to share environment variables across files and services
---

The [`@import()` root decorator](/reference/root-decorators/#import) allows you to import schema and/or values from other sources (currently just `.env` files), making it easy to share config across services within a monorepo, split up large schemas, or reuse pre-defined schemas. Multiple `@import()` calls may be used, and an imported source may itself import more sources.

**Basic examples:**
```env-spec
# @import(./.env.imported)                 # import specific file
# @import(./env-dir/)                      # import directory
# @import(./.env.partial, pick=[K1, K2])   # import specific keys
# @import(~/.env.shared)                   # import from home directory
```

## Import source types

The first argument to `@import()` specifies where to look for file(s) to import.
Currently only local file imports are supported, but we plan to support importing over http in a style similar to Deno's http imports.

For now, all imported files must be `.env` files (and may contain @env-spec decorators), but in the future, we may also support other formats (e.g., JSON, YAML, etc.) or even JS/TS files.

### Single file

- Path must begin with `./`, `../`, `/`, or `~/`
- Imported file name must be begin with `.env.`

```env-spec
# @import(./.env.common)
```

:::tip[Home directory (`~`)]
You can import files from your home directory using the `~/` prefix. This is useful for sharing personal configuration across multiple projects without placing files inside any specific repo.

```env-spec
# @import(~/.env.shared)
```
:::

### Directory

- Path must begin with `./`, `../`, `/`, or `~/`
- Path must end with a trailing `/`
- Multiple `.env.*` files will be detected and loaded, based on the current environment flag, similar to what happens in the current directory (see [environments guide](/guides/environments#loading-environment-specific-env-files))
- The environment flag value will be inherited, unless another `@currentEnv` is defined within the directory's `.env.schema`

```env-spec
# @import(../shared-config-dir/)
```

:::tip[File vs directory: which should I import?]
- **Import a single file** when you want exactly that file's contents: a shared `.env.common`, or specific keys from another schema via `pick`.
- **Import a directory** (trailing `/`) when you want everything that directory would load on its own: its `.env.schema` **plus** sibling `.env`, `.env.local`, and environment-specific files, resolved by the current environment flag.

This distinction matters when pulling in a root or sibling package. Importing its `.env.schema` file alone brings only the schema; importing the package's **directory** also brings its values (including `.env.local` and `.env.[currentEnv]`). For the common "import the repo root" case in a monorepo, use the directory form (`# @import(../../)`).
:::

{/*
### HTTP URL (👀 coming soon)

- Protocol must be https
- File name must start with `.env.`
- If URL does not exist or is not accessible, will cause a loading error

```env-spec
# @import(https://env-schema.varlock.dev/platforms/.env.vercel)
```

### JS package registries (👀 coming soon)

A registry (npm, jsr, etc.), package name, version, and path can be used to import a schema from a public package.

```env-spec
# @import(npm:@varlock/vercel-integration@1.2.3/.env)
```
 */}

## Partial imports

By default, all items will be imported, but you can restrict which keys are brought in:

- `pick=[...]` imports only an allowlist of keys
- `omit=[...]` imports everything except a denylist of keys
- You can't use both in one import, and both accept simple globs (`*`, `?`)
- If there is a chain of imports, an item is only imported if every ancestor import includes it (filters intersect)

```env-spec
# @import(./.env.imported, pick=[KEY1, KEY2])   # allowlist
# @import(./.env.imported, pick=[API_*])        # globs work too
# @import(./.env.imported, omit=[SECRET])       # denylist
```

:::caution[Positional keys are deprecated]
Older docs and configs list keys as positional args (`@import(./.env.imported, KEY1, KEY2)`). This still works but is deprecated and will warn. Use `pick=[KEY1, KEY2]` instead.
:::

## Conditional imports

While you can use the [`@disable`](/reference/root-decorators/#disable) root decorator to disable a file _from within that file_, you can also use the `enabled` parameter of the `@import()` decorator to conditionally load the file.

The `enabled` parameter accepts any expression that evaluates to a boolean. It can be combined with partial imports to only import specific keys from a file.

**Example:**
```env-spec title=".env.schema" /\(enabled=.*\)\\)/
# Combine with partial imports
# @import(./.env.features, pick=[FEATURE_X], enabled=eq($ENABLE_X, "true"))
# ---
ENABLE_X=true
```

## Optional imports

By default, `@import()` will cause a loading error if the specified file or directory does not exist. You can use the `allowMissing` parameter to make an import optional - if the file or directory doesn't exist, it will be silently skipped without causing an error.

The `allowMissing` parameter accepts a boolean value (defaults to `false`). It can be combined with partial imports and the `enabled` parameter.

**Example:**
```env-spec title=".env.schema"
# Import if exists, skip if not
# @import(./.env.local, allowMissing=true)
```

**Combine with other parameters:**
```env-spec title=".env.schema"
# Optional partial import with conditional loading
# @import(./.env.features, pick=[FEATURE_X], enabled=true, allowMissing=true)
```

## Import precedence and merging multiple sources

Varlock is designed to load multiple definitions for a single item and merge them together.
The common case would be taking schema info from `.env.schema` and overriding a value from another source (e.g., `.env.local`, `.env.production`, etc.),
but there are many cases where root decorators, item decorators, and descriptions may be merged as well.

To do this, we usually walk our data sources in decreasing order of precedence, until we find something defined for the value/decorator/etc we are evaluating.

**Precedence rules are:**
- Imported files are processed in order, with later imports overriding previous imports
- Definitions and root decorators in the importing file override those in files it imports
- For a directory, the precedence order is `.env.schema` < `.env` < `.env.local` < `.env.{currentEnv}` < `.env.{currentEnv}.local`

For example, given a `.env.local` and a `.env.schema` that imports 2 files:
```env-spec title=".env.schema"
# @import(./.env.import1)
# @import(./.env.import2)
```

The precedence order would be `.env.import1` < `.env.import2` < `.env.schema` < `.env.local`.

Meaning if there was a value for `ITEM` in all 4 files, the final value used would be the one from `.env.local`.


## More details
- Root decorators that affect individual items (e.g., `@defaultRequired`) affect only the items that are defined in the file, not those in imported files
- An item with no value at all (e.g., `ITEM=`) will be skipped when looking for a value / function to use, but its presence can be used to add other decorators/description to the item
- If an imported file is marked with [`@disable`](/reference/root-decorators/#disable), it and any files it imports are skipped entirely
- Importing the same source from multiple places (a "diamond") is fine: it is loaded once and reused

## When things go wrong

### `environment flag "..." must be defined within this schema`

If you use [`@currentEnv`](/reference/root-decorators/#currentenv) to point at a variable (e.g. `# @currentEnv=$DEPLOY_ENV`) and that variable is only brought in via a **partial** `@import()`, varlock validates the env flag during schema initialization, before imported values are merged. The flag must be **defined in the same `.env.schema` file** that declares `@currentEnv`, not only in an imported file.

This commonly appears in monorepos when a sub-package imports shared keys from a parent schema:

```env-spec title=".env.schema (sub-package)"
# @currentEnv=$DEPLOY_ENV
# @import(../../../, pick=[DEPLOY_ENV, AWS_REGION])
# ---
MY_SERVICE_URL=...
```

Running `varlock load` fails with:

```txt
environment flag "DEPLOY_ENV" must be defined within this schema
```

**Fixes:**

- Define the env flag locally in the file that uses `@currentEnv`, even if the value comes from elsewhere:

```env-spec title=".env.schema (sub-package)"
# @currentEnv=$DEPLOY_ENV
# @import(../../../, pick=[DEPLOY_ENV, AWS_REGION])
# ---
DEPLOY_ENV=
MY_SERVICE_URL=...
```

- Move `@currentEnv` to the shared schema where the flag is already defined
- Import the full directory (omit the key list) if the sub-package should inherit the parent's `@currentEnv` handling

### Circular imports

A circular chain of imports (e.g. `a` imports `b` which imports `a`) is not allowed. Varlock detects the cycle and fails with an explicit error showing the chain:

```txt
Circular import detected: a/.env.schema -> b/.env.schema -> a/.env.schema
```
