Packages¶
FastFlowTransform packages let you reuse models and macros across projects.
A package is just another FFT project (or mini-project) whose models/ you want to treat as if they were part of your current project. You wire it in via a packages.yml file in your project root.
Typical use cases:
- A shared staging layer (e.g. CRM / ERP cleaning) used by multiple teams.
- A central macro library (casting helpers, email parsing, date tricks).
- A “starter kit” of canonical marts that downstream projects can add on top of.
Packages can come from:
- A local path on disk.
- A git repository (with optional branch/tag/commit + subdir).
1. High-level behavior¶
When you declare packages in packages.yml:
-
FFT loads your main project as usual.
-
It runs the package resolver:
-
For each entry in
packages.ymlit:-
locates the package:
-
local: resolves the
pathon disk. - git: clones/fetches a repo into
.fastflowtransform/packagesand checks out the requested ref. -
reads that package’s
project.yml(the package manifest): -
name,version, optionalfft_version, optionaldependencies, optionalmodels_dir. -
validates:
-
manifest
namematches thenamefrompackages.yml. fft_version(if present) is compatible with the running FFT version.- the spec’s
versionconstraint (if present) is satisfied bymanifest.version. - package dependencies (if declared) are satisfied by other packages.
- Writes a
packages.lock.ymlwith pinned sources (paths / git commit SHAs).
-
-
For each resolved package, FFT:
-
decides which directory to treat as its
models_dir:packages.yml:models_diroverridesproject.yml:models_dir, default"models".- loads SQL / Python models and macros from that directory.
- registers them into the same namespace as your own models.
From inside your project you can:
ref('users_base.ff')even ifusers_base.ff.sqlphysically lives in a package folder.- Use macros defined in
models/macros/*.sqlinside a package.
There is no special syntax for package references; once loaded, package models look like any other model.
2. Minimal setup¶
2.1. Create a reusable package¶
A package is structured like a normal FFT project, but consumers mainly care about its models/ and macros.
shared_package/
project.yml
models/
macros/
shared_utils.sql
staging/
users_base.ff.sql
Example project.yml in the package:
name: shared_package
version: "0.1"
# Where this package’s models live relative to the package root.
# This can be overridden by packages.yml in the consumer project.
models_dir: models
# Optional: constrain which FFT core versions can use this package.
# If omitted, any FFT version is allowed.
fft_version: ">=0.6.0,<0.7.0"
# Optional: dependencies on other packages (by name) if you compose packages.
# These are validated against the set of packages declared in the consumer’s packages.yml.
dependencies: []
Example macro (models/macros/shared_utils.sql):
{# Shared SQL macros for the package #}
{%- macro email_domain(expr) -%}
lower(regexp_replace({{ expr }}, '^.*@', ''))
{%- endmacro -%}
Example staging model (models/staging/users_base.ff.sql):
{{ config(
materialized='view',
tags=[
'pkg:shared_package',
'scope:staging',
'engine:duckdb',
'engine:postgres',
'engine:databricks_spark',
'engine:bigquery',
],
) }}
with raw_users as (
select
cast(id as integer) as user_id,
lower(email) as email,
cast(signup_date as date) as signup_date
from {{ source('crm', 'users') }}
)
select
user_id,
email,
{{ email_domain("email") }} as email_domain,
signup_date
from raw_users;
This package expects the consumer project to define source('crm','users').
2.2. Declare the package in your project¶
In your main project:
my_project/
project.yml
packages.yml ← new
models/
seeds/
…
Create packages.yml:
packages:
- name: shared_package
path: "../shared_package" # resolved relative to this file
models_dir: "models" # optional; defaults to "models"
nameLogical name for the package, taken from the package’s ownproject.yml. This must match the manifest; it’s used for logs, diagnostics, and dependency checks.pathFilesystem location of the package root, resolved relative to the directory containingpackages.yml.models_dir(optional) Subdirectory within the package root that contains the package’s models. Defaults tomodels. If bothproject.yml:models_dirandpackages.yml:models_dirare set,packages.ymlwins.
2.3. Use package models in your project¶
Now, in my_project/models/marts/mart_users_from_package.ff.sql:
{{ config(
materialized='table',
tags=['example:packages_demo', 'scope:mart', 'engine:duckdb'],
) }}
with base as (
select
email_domain,
signup_date
from {{ ref('users_base.ff') }} -- defined in the shared_package
)
select
email_domain,
count(*) as user_count,
min(signup_date) as first_signup,
max(signup_date) as last_signup
from base
group by email_domain
order by email_domain;
Run as usual:
fft seed . --env dev_duckdb
fft run . --env dev_duckdb
fft dag . --env dev_duckdb --html
The DAG will show something like:
crm.users (source) → users_base.ff (from package) → mart_users_from_package.ff (local)
3. packages.yml – configuration reference¶
packages.yml must live in the project root, next to project.yml:
my_project/
project.yml
packages.yml
models/
…
Top-level structure:
packages:
- name: ...
...
You can declare both path-based and git-based packages. Exactly one of path or git must be set per package.
3.1. Path packages¶
packages:
- name: shared_package
path: "../shared_package" # relative or absolute
models_dir: "models" # optional
# optional semver constraint on the package’s manifest version:
version: ">=0.1.0,<0.2.0"
Fields:
name(required) Must matchproject.yml:nameinside the package root.path(required for path packages) Relative or absolute path to the package root. Resolved relative topackages.yml.models_dir(optional) Models directory inside the package root. Default"models".version(optional) Semver constraint for the package’sproject.yml:version. See 4.2 Version constraints.
3.2. Git packages¶
packages:
- name: shared_package_git
git: "https://github.com/fftlabs/fastflowtransform.git"
# Directory inside the repo that contains the package
subdir: "examples/packages_demo/shared_package_git_remote"
# Optional ref selectors (only one needs to be set; see notes below)
ref: "main" # generic alias (branch / tag / commit)
# rev: "abc1234" # explicit commit SHA
# tag: "v0.6.11" # tag name
# branch: "main" # branch name
models_dir: "models"
# Optional semver constraint on the package's manifest version
version: ">=0.1.0,<0.2.0"
Fields:
name(required) Must matchproject.yml:namein the package subdir.git(required for git packages) Git URL (HTTPS or SSH, depending on your environment).subdir(optional but recommended) Path inside the repo that should be treated as the package root (relative to the repo root). If omitted, the repo root itself is the package root.ref(optional) Generic user-facing selector (branch, tag, or commit). If you don’t specify a more precise field (rev/tag/branch),refis mapped internally torevand passed directly togit checkout.rev/tag/branch(optional) More explicit selectors, used in preference torefif set.models_dir(optional) Models directory inside thesubdirroot (default"models").version(optional) Semver constraint for the package’sproject.yml:version.
Resolution rules:
- FFT clones/fetches git packages into:
.fastflowtransform/packages/git/<slug>/repo
where <slug> encodes the package name and git URL.
* For each package, FFT:
- clones the repo (if missing),
- attempts a
git fetch --all(best effort) if it already exists, -
runs
git checkout <ref>using:revortagorbranch(first non-empty),- or
HEADif none are provided.
If Git commands fail, you get targeted error messages:
- Missing git binary → “git executable not found…”
- Auth issues → “authentication error…”
- Wrong repo / URL → “repository not found…”
- Bad ref / branch / tag → “requested ref/branch/tag does not exist…”
3.3. Shorthand mapping form¶
For local packages you can use a shorter mapping form:
# Equivalent to packages: [ { name: shared_package, path: ../shared_package } ]
shared_package: "../shared_package"
other_pkg:
path: "../other"
models_dir: "dbt_models"
Internally this is normalized to the explicit packages: list.
3.4. Multiple packages¶
You can declare multiple packages:
packages:
- name: shared_staging
path: "../shared_staging"
- name: analytics_macros
git: "https://github.com/my-org/analytics-macros.git"
subdir: "packages/sql_macros"
models_dir: "models"
All models/macros from all packages are loaded into the same logical project.
4. Manifests, versions & dependencies¶
4.1. Package manifests (project.yml inside the package)¶
Every package has its own project.yml at the package root (or package subdir for git packages):
name: shared_package
version: "0.1.0"
models_dir: "models" # optional; may be overridden by packages.yml
fft_version: ">=0.6.0,<0.7.0" # optional
dependencies:
- name: other_shared_pkg
version: ">=1.0.0,<2.0.0"
optional: false
FFT uses this manifest for:
nameMust match thenamefrompackages.yml.versionCompared to the spec’sversionconstraint, if provided.fft_version(optional) Semver constraint against the running FFT version. If your package only supports certain FFT versions, set this. If the constraint is not satisfied, resolution fails with a clear error.models_dir(optional) Default path for models within the package root; overridden bypackages.yml:models_dirif set.dependencies(optional) A list of other packages (by name) this package expects to be present in the same project.
Each dependency entry may include:
name– required, another package’s name.version– optional semver constraint on that package’sproject.yml:version.optional– iftrue, missing dependency is allowed; otherwise it is an error.
Resolution validates that:
- Every non-optional dependency
nameis present in the set of packages declared in the consumer’spackages.yml. - If a
versionconstraint is given for a dependency, the resolved dependency’s version satisfies it.
4.2. Version constraints¶
Package specs and dependencies support a tiny semver subset. Version strings must be in MAJOR.MINOR.PATCH form (e.g. 1.2.3).
Supported constraint forms:
- Bare version:
"1.2.3" # equivalent to "==1.2.3"
- Comparators (can be combined with commas or spaces):
">=1.2.0,<2.0.0"
">1.0.0 <=2.0.0"
^(caret) ranges:
"^1.2.3" # >=1.2.3,<2.0.0
"^0.3.0" # >=0.3.0,<0.4.0
"^0.0.4" # >=0.0.4,<0.0.5
~(tilde) ranges:
"~1.2.3" # >=1.2.3,<1.3.0
The resolver checks:
- Consumers → packages:
packages.yml:versionvs package’sproject.yml:version. - Package → package:
dependencies[].versionvs the dependent package’s version. - Package → FFT core:
project.yml:fft_versionvs the running FFT version.
If a constraint fails, you get a clear runtime error showing which package and which constraint failed.
5. What gets loaded (and what doesn’t)¶
When FFT loads a package, it will:
Loads:
project.ymlmanifest (for name, version, fft_version, dependencies, models_dir).- SQL models:
*.ff.sqlunder the resolvedmodels_dir. - Python models:
*.ff.pyundermodels_dir. - SQL macros: under
models_dir/macros/(e.g.macros/shared_utils.sql). - Python helpers/macros: under
models_dir/macros_py/(same mechanism as the main project).
Does NOT load / run automatically:
profiles.ymlfrom the package — the consumer project’s profiles are always used.- Seeds / sources defined in the package — these are still local to the consumer project.
- Tests declared in the package’s
project.yml— only tests in the consumer project’sproject.ymlare run onfft test.
In practice, package models still call
source('…')andref('…'). The consumer project is responsible for defining sources / seeds / additional models.
6. Name resolution & conflicts¶
6.1. Model names¶
Once loaded, a package model is just a regular model:
- It has a logical name (e.g.
users_base.ff). - It is registered in the same global registry as your local models.
Rules:
ref('<model_name>')never has a package prefix. You always use the model name alone.-
Model names must be globally unique across:
-
your main project,
- all loaded packages.
If two models share a name (e.g. users_base.ff in both main and package), FFT will fail loading with a clear “Duplicate model name” error. You must rename or delete one of them.
6.2. Macros¶
Macros from packages and local macros all end up in the same Jinja environment.
- Name collisions are possible.
- “Last one wins” — whichever macro is registered last overrides earlier ones.
Best practice:
- Prefix macro names with a package-ish prefix:
shared_email_domain, etc. - Or use explicit
{% import 'macros/shared_utils.sql' as shared %}and callshared.email_domain()from consumer models.
7. Lock file: packages.lock.yml¶
After successful resolution, FFT writes a packages.lock.yml next to packages.yml:
fft_version: "0.6.11"
packages:
- name: shared_package
version: "0.1.0"
source:
kind: path
path: "/absolute/path/to/shared_package"
- name: shared_package_git
version: "0.1.0"
source:
kind: git
git: "https://github.com/fftlabs/fastflowtransform.git"
rev: "abc1234deadbeef..." # resolved commit SHA
subdir: "examples/packages_demo/shared_package_git_remote"
Today the lock file is:
- Written after each successful resolution.
- Useful for diagnostics, reproducibility, CI logs, etc.
(Resolution is still driven by packages.yml; the lockfile does not yet drive resolution itself.)
8. CLI: fft deps¶
The deps command inspects packages for your project and shows their resolved status:
fft deps .
Behavior:
-
Resolves the project directory.
-
Runs the full package resolver (same as
fft runwould): -
locates local path packages,
- clones/fetches git packages,
- loads
project.ymlmanifests, - validates version constraints and dependencies,
-
writes
packages.lock.yml. -
Prints a small report for each package:
Project: /path/to/my_project
Packages:
- shared_package (0.1.0)
kind: path
path: /abs/path/to/shared_package
models_dir: models -> /abs/path/to/shared_package/models
status: OK
- shared_package_git (0.1.0)
kind: git
git: https://github.com/fftlabs/fastflowtransform.git
rev: abc1234deadbeef...
subdir: examples/packages_demo/shared_package_git_remote
models_dir: models -> /abs/.../repo/examples/packages_demo/shared_package_git_remote/models
status: OK
- Exits with non-zero status if any package’s
models_diris missing or invalid.
This is the easiest way to debug:
- git connectivity / credentials,
- bad refs (
tag/branch/rev), - missing
project.ymlin a package, - version constraint mismatches,
- missing
models_dirdirectories.
9. DAGs, caching, selectors¶
Once packages are resolved, FFT essentially treats:
“main project + all packages” as one large logical project.
9.1. DAG & docs¶
fft dagand the generated HTML docs include package models and edges between them and your local models.- You can inspect
.fastflowtransform/target/manifest.jsonif you need to distinguish package vs local models programmatically (nodes carry metadata like their originating package).
9.2. Caching¶
Build caching behaves the same for package models as for local ones:
-
Fingerprints incorporate:
-
SQL/Python source of the package model,
- upstream dependencies,
- environment, etc.
- Changing a package model’s code changes its fingerprint and invalidates cache for that model and its downstream dependents.
9.3. Selectors & tests¶
Selectors (--select, --exclude) are package-agnostic:
- You can tag package models:
{{ config(
tags=['pkg:shared_package', 'scope:staging'],
) }}
- Then:
fft run . --env dev_duckdb --select tag:pkg:shared_package
You can define tests in your main project for tables produced by package models:
tests:
- type: not_null
table: users_base
column: email
tags: [example_packages_demo]
Only the tests defined in the consumer project’s project.yml are executed on fft test.
10. Best practices & pitfalls¶
10.1. Treat packages like libraries¶
- Always set a
versionin the package’sproject.yml. - Use tags/releases/branches on the git repo for meaningful versions.
- Use
packages.yml:versionconstraints to avoid accidental breaking upgrades.
10.2. Keep responsibilities clear¶
- Package: shared semantics (cleaning, typing, derived fields), stable over time.
- Consumer project: product/report-specific marts and joins.
10.3. Avoid tight coupling to specific schemas¶
- Use generic
source('domain','table')names in packages. - Document expected sources in the package README.
- Let each consumer wire those to their actual tables via their own
sources.yml.
10.4. Tag and namespace thoughtfully¶
- Tag package models with something like
pkg:<name>to make them easy to select/exclude. - Use macro namespaces or prefixes to reduce collisions.
10.5. Common errors¶
-
Package root … has no project.ymlYour package directory (or gitsubdir) is wrong. Pointpath/subdirat the folder that actually contains the package’sproject.yml. -
Git errors about authentication or unknown revision Check your git URL/credentials and branch/tag/commit.
fft depswill show the raw git error stderr to help you debug. -
Version mismatch errors Align:
-
the package’s
versionand yourpackages.yml:version, - the package’s
fft_versionand your installed FFT version.
In short: packages let you compose FFT projects like libraries, with both local and git-backed sources, basic versioning, and a resolver + lockfile that make behavior explicit and debuggable.