Skip to content

Getting started

At the end of this tutorial you will have a Lakana instance running on your own machine, three tables built by a sample project you deployed, and one query answered from those tables. You do not need Docker or a database server.

Lakana is pre-beta: the API and the data layout may change between 0.x releases, so do not put production data in it yet. It has no web interface yet: everything in this tutorial is done in a terminal with the CLI, and the interface is planned after the public beta.

You need Python 3.11 or newer (on macOS: Python 3.13 or newer) and git. With an older Python on macOS, lakana server start --local stops before it starts anything and says so. Once Lakana is installed, plan on ten minutes.

Install

Lakana is one Python package, published on PyPI as a pre-beta (0.x). The server extra adds what a local instance needs:

pip install "lakana[server]"

To check that a download is the one Lakana published, see Verify a download.

To work on Lakana itself, or to try the development version, install from a checkout of the repository instead. You need uv, a Python package manager:

git clone https://github.com/lakana-labs/lakana.git
cd lakana
uv sync --all-packages
source .venv/bin/activate

The last line puts the lakana command on your path, for this terminal only. Run it again in every new terminal you open below, from inside the checkout. A checkout is a development build: at the end of its output, lakana init prints a note about pinned versions for CI. You can ignore it in this tutorial.

Start a local instance

A local instance is a complete Lakana on your machine: the server, a worker that runs your queries and builds, and a place to keep tables. Start it:

lakana server start --local

The instance stays in the foreground and keeps this terminal. It prints no line per request, so the line that says it is ready is the last one you see. When it is ready, it prints a line like this one:

Lakana is running at http://127.0.0.1:8765 — profile 'local', environment 'dev'. Try: lakana sql "SELECT 42"

To see a line for every request the server answers, start with the option --log-requests. To see more or fewer of the other lines, set LAKANA_LOG__LEVEL (for example to DEBUG) in front of the command; local mode passes it to the server and the worker.

The worker has a memory limit, 4 GB by default (LAKANA_WORKER__MEMORY_LIMIT; off turns it off). It covers the worker and the tasks and queries it started. On Linux, a worker that goes over it ends the largest running query or Python task first (that one fails, the worker and the rest go on); if the worker is still over the limit after that, it stops and is started again (local mode does that, at most three times in five minutes, and prints one line), and the query or task that was running is marked failed. The worker says which limit applies in one line at start. On Linux it measures the worker and its tasks; on macOS only the worker process, and on Windows nothing, and then the line says the limit is not enforced. You can ignore that line. In local mode a very fast query can still use up the machine's memory before the worker notices; the Compose bundle adds a limit that the kernel enforces.

Leave this terminal as it is. Open a second terminal for the rest of the tutorial, and activate the environment there too if you installed from a checkout.

To see that it worked, ask for the version. The command prints the version of the lakana command and, because an instance is running, the version of the instance:

lakana --version

The versions, and the port in the second line, differ on your machine:

lakana 0.8.1.dev6+g6af3c5d2a
instance http://127.0.0.1:8765: 0.8.1.dev6+g6af3c5d2a

Note

If the start fails with "Port 8765 is in use.", another program uses that port. Stop that program and start again, or pass another port with --port; the commands below then find your instance on it. If it fails with "Lakana is already running", an instance you started earlier still runs; its message names the address. Use it, or stop it with lakana server stop (add --data-dir if you started it with one) and start again.

An environment is a separate place where a project is deployed and its tables live. A local instance has one, called dev. Every command below that needs an environment names it with --env dev.

Create a project

A project is a git repository of SQL and Python files. Lakana reads it from git, so every table you build can be traced to a commit.

A local instance only accepts projects under your home directory. The first command goes there, so that you do not create the project in /tmp. (A project elsewhere is refused when you register it, with a message that tells you to move it under your home directory.)

cd ~
lakana init analytics
cd analytics

lakana init writes a small sample, and lists what to do next. Yours shows your own folder, and a note about the development build if you installed from a checkout:

Created a Lakana project in /home/you/analytics, with a small sample: generated readings from three weather stations, three SQL assets built from them, and one check.
Next:
  cd /home/you/analytics
  git init && git add -A && git commit -m 'Initial project'
  lakana repo add analytics --kind local --url /home/you/analytics
Then the two commands that build the sample:
  lakana deploy --env dev
  lakana run --env dev
Then look at the result:
  lakana sql "SELECT * FROM gold.station_ranking ORDER BY warmth_rank"
Local repositories must lie under LAKANA_REPO_ROOTS (local mode allows your home directory).
If 'git commit' says it does not know who you are, set a name and email first: git config --global user.name 'Your Name' && git config --global user.email you@example.com
Note: lakana 0.8.1.dev6+g6af3c5d2a is a development build, so the CI install `lakana==0.8.1.dev6+g6af3c5d2a` can't be installed from the index. Ignore this until you use CI; then edit the pinned version in .github/workflows/lakana-deploy.yml and .gitlab-ci.yml.

You now have a folder analytics with a lakana.yaml file, which holds the project's settings. lakana init also wrote files for CI and for Python assets; this tutorial leaves them as they are. The rest of what it wrote is the sample, and the next sections read it, so that you know what you are about to build. You do not need to write anything. (To start a project without the sample, run lakana init --no-sample.)

The sample

An asset is one file that describes one table. Lakana runs the file and stores the result as the table. Its path decides the table's name: assets/bronze/readings.sql becomes the table bronze.readings. Here bronze is the namespace, a folder of tables, and readings is the name.

Many teams sort tables into layers. Bronze tables hold data as it comes in. Silver tables hold data that has been cleaned or summed up from bronze. Gold tables are the ones people read. Lakana does not require this; the sample follows it because it is a common way to keep order.

The first asset, assets/bronze/readings.sql, is the source. It holds eighteen temperature readings from three weather stations. The query generates them, so you need no data file, and the table is the same on every machine:

/* lakana
description: Eighteen generated readings from three weather stations, six days each.
columns:
  station_id: The station that took the reading.
  day: The day of the reading.
  temp_c: The temperature in degrees Celsius.
*/
SELECT
  stations.station_id,
  DATE '2026-01-01' + CAST(days.n AS INTEGER) AS day,
  CAST(stations.base_c + (days.n % 3) - 1 AS DOUBLE) AS temp_c
FROM (VALUES ('A1', 4.0), ('B2', 1.0), ('C3', 9.0)) AS stations(station_id, base_c)
CROSS JOIN (VALUES (0), (1), (2), (3), (4), (5)) AS days(n)

The comment at the top is the asset's documentation: what the table holds, and what each column means. The project's settings require a description for every asset, and Lakana refuses a deploy without one. The rest of the file is a single SELECT query.

The second asset, assets/silver/station_temperatures.sql, reads bronze.readings and sums it up for each station:

/* lakana
description: The lowest, highest and average temperature of each station.
columns:
  station_id: The station that took the readings.
  readings: How many readings the station took.
  min_temp_c: The lowest temperature in degrees Celsius.
  max_temp_c: The highest temperature in degrees Celsius.
  avg_temp_c: The average temperature in degrees Celsius.
*/
SELECT
  station_id,
  COUNT(*) AS readings,
  MIN(temp_c) AS min_temp_c,
  MAX(temp_c) AS max_temp_c,
  AVG(temp_c) AS avg_temp_c
FROM bronze.readings
GROUP BY station_id

The third, assets/gold/station_ranking.sql, reads the silver table and puts the stations in order, from the warmest to the coldest:

/* lakana
description: The stations from the warmest to the coldest.
columns:
  station_id: The station.
  avg_temp_c: The station's average temperature in degrees Celsius.
  warmth_rank: 1 for the warmest station, 2 for the next, and so on.
*/
SELECT
  station_id,
  avg_temp_c,
  RANK() OVER (ORDER BY avg_temp_c DESC) AS warmth_rank
FROM silver.station_temperatures

Because each asset reads the table of the one before, Lakana knows the order in which to build them. You never state the order yourself.

The check

A check is a file that says what must be true of a table. It is one SELECT that returns the rows that are wrong; a table with no wrong rows passes. Its path names the table it checks: checks/silver/station_temperatures/plausible_range.sql is a check of silver.station_temperatures. It looks for a station whose average temperature is impossible, which means a faulty sensor:

/* lakana
description: An average below -90 or above 60 degrees is a sensor fault.
*/
SELECT station_id, avg_temp_c
FROM silver.station_temperatures
WHERE avg_temp_c < -90 OR avg_temp_c > 60

Every run tests a table after it builds it. If this check returned a row, the run would fail and the table would be set back to what it was before. With your three stations it returns none. The how-to guide Check your data shows what a failing check looks like.

Commit

Git can only commit if it knows who you are. If you have used git on this machine before, it does, and you can skip to the next block. If not, git commit stops with a message that asks for your name and email; lakana init printed a line about this too ("If 'git commit' says it does not know who you are ..."). Set them once, with your own values:

git config --global user.name "Your Name"
git config --global user.email "you@example.com"

Now turn the folder into a git repository and commit your files:

git init
git add -A
git commit -m "Initial project"

A deploy reads a commit, not the files in your folder, so a change that is not committed is not deployed. Git may also print a long hint about the name of the first branch. It is harmless.

Deploy

To deploy is to give Lakana a commit of a project for one environment. Lakana checks the project, and if the checks pass, records it as the version that runs there. The first command tells Lakana where the repository is. The second deploys the latest commit to dev.

lakana repo add analytics --kind local --url "$PWD"
lakana deploy --env dev

You will see Registered repository analytics, then the deploy: its identifier, the commit it deployed, a table of what changed and, last, a hint that names the next step. Yours looks like this, with a different identifier and commit:

Queued deploy 71ddefdc-6a57-4c5b-bac3-cbacc2fb4759
Deployed analytics in dev at ca36c93fe2337b324df3ab8d03cac9b9d1ed1420
┏━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ change ┃ table                       ┃
┡━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
│ added  │ bronze.readings             │
│ added  │ gold.station_ranking        │
│ added  │ silver.station_temperatures │
└────────┴─────────────────────────────┘
Checks:
  silver.station_temperatures: plausible_range
Hint: run 'lakana run --env dev' to run what you just deployed.

All three tables are listed as added, but no table exists yet. A deploy only records what to build. Under Checks: it lists the check it found, and the hint names the next step.

Run

A run builds the tables of the deployed version, in the order their dependencies need. It is the command the hint named:

lakana run --env dev

The command prints one line each time an asset changes state and waits until the run is done. It ends with a summary like this one. The run identifier above it differs on your machine, and so may the durations:

Run e6c654de-926d-4941-896d-909ead7cd45d succeeded
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━━┳━━━━━━┳━━━━━━━┓
┃ asset                       ┃ status    ┃ attempt ┃ duration ┃ rows ┃ error ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━╇━━━━━━━━━╇━━━━━━━━━━╇━━━━━━╇━━━━━━━┩
│ bronze.readings             │ succeeded │ 1/3     │ 0.0s     │ 18   │       │
│ gold.station_ranking        │ succeeded │ 1/3     │ 1.0s     │ 3    │       │
│ silver.station_temperatures │ succeeded │ 1/3     │ 0.0s     │ 3    │       │
└─────────────────────────────┴───────────┴─────────┴──────────┴──────┴───────┘

silver.station_temperatures   passed   1 of 1 checks passed
  plausible_range             passed

All three assets succeeded. bronze.readings has eighteen rows, one for each reading, and the other two have three, one for each station. The last two lines are the check: after silver.station_temperatures was built, plausible_range ran against it and passed.

Query

Ask the gold table for the stations, from the warmest to the coldest. The command is the last one that lakana init suggested:

lakana sql "SELECT * FROM gold.station_ranking ORDER BY warmth_rank"

You will see this result:

┏━━━━━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━━━━┓
┃ station_id ┃ avg_temp_c ┃ warmth_rank ┃
┡━━━━━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━━━━━━━┩
│ C3         │ 9.0        │ 1           │
│ A1         │ 4.0        │ 2           │
│ B2         │ 1.0        │ 3           │
└────────────┴────────────┴─────────────┘
3 rows

Station C3 averaged 9 degrees, A1 averaged 4 and B2 averaged 1, so C3 is first. CI runs every command on this page on every change. It checks the query result above and the lines of the other outputs that stay the same: the tables of the deploy, and the status and row counts of the run. Identifiers, commits, versions and durations differ on every machine and are not checked.

A column may carry the name of its table or an alias, as in any SQL: station_ranking.station_id, gold.station_ranking.station_id or r.station_id after FROM gold.station_ranking AS r.

Stop the instance

Go to the first terminal and press Ctrl-C, or stop the instance from any terminal:

lakana server stop

The instance shuts down and the command says so; if none runs, it says that instead and exits without an error. Your tables stay where they are, and lakana server start --local brings the instance back with them.

Start over

To go through this tutorial again, or to throw away what you built, stop the instance as above and reset it. The command names what it will delete and asks before it does:

lakana server reset --local

It deletes the instance's database and the tables' files, and with them every table, secret and token of this instance (the number of tables is in its question). It does not touch your project: the folder analytics and its git history stay. It refuses while the instance runs, and in any folder that is not a local instance's data directory. Afterwards, lakana server start --local is a first run again, and this page works from the "Create a project" step: delete the folder analytics first, or give the new project another name. In a script, pass --yes to skip the question.

Where next