> For the complete documentation index, see [llms.txt](https://v2.dataos.info/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://v2.dataos.info/build/get-started/quickstart.md).

# Quickstart

This is the fastest way to see the whole build pattern. You take three raw CSV files, turn them into a tested Data Product with one published metric, and run the lot locally. Nothing touches DataOS or a cloud warehouse until you decide to deploy.

The example is small on purpose: a retail team selling in 4 regions wants one trusted answer to "daily revenue by region" instead of 4 spreadsheets that disagree. By the end you have that metric, served through auto-generated APIs.

## Prerequisites

You need three things installed. Each takes a minute; [LDK setup](/build/get-started/prerequisites.md) has the full detail if a step fails.

* **Python 3.10**.
* **Docker**, to run a local Postgres in one command.
* **Vulcan**. Download the wheel from [LDK setup](/build/get-started/prerequisites/ldk-setup.md), place it in your working folder, then install it into a virtual environment:

  ```bash
  python3.10 -m venv .venv && source .venv/bin/activate
  pip install "./vulcan-0.228.1.28-py3-none-any.whl[postgres]"
  ```

Vulcan runs entirely on your machine. You do not need real DataOS access or an [API token](https://v2.dataos.info/references/key-concepts/api-tokens) for this Quickstart. Vulcan does always require `DATAOS_TENANT_ID` in the environment, even locally, so export any placeholder value before running commands: `export DATAOS_TENANT_ID=local`.

## 1. Start a local engine

Create a working folder. Copy the Postgres compose file from [LDK setup](/build/get-started/prerequisites/ldk-setup.md) (under "Set up your engine") to `docker/docker-compose.warehouse.yml`, then start it:

```bash
docker network create vulcan
docker compose -f docker/docker-compose.warehouse.yml up -d
```

This starts a `warehouse` Postgres on `localhost:5433` that Vulcan writes to. State is kept locally in DuckDB, so you do not need a second database.

## 2. Initialize the project

```bash
vulcan init
```

Choose `DEFAULT` for the project type and `postgres` for the engine. Vulcan scaffolds the folders you will fill in: `models/`, `seeds/`, `dq/`, `audits/`, `tests/`, `macros/`, `plugins/`, `policies/`, `config.yaml`, `usage.yml`, and `agreement.md`.

Open `config.yaml` and point it at the local Postgres:

<details>

<summary><code>config.yaml</code></summary>

```yaml
name: workspace
description: Local quickstart product tracking daily revenue by region.

users:
  - username: vulcan
    type: OWNER

default_gateway: postgres
gateways:
  postgres:
    connection:
      type: postgres
      host: localhost
      port: 5433
      database: warehouse
      user: vulcan
      password: vulcan
    state_connection:
      type: duckdb
      database: ./.state/vulcan.db
model_defaults:
  dialect: postgres
  start: 2025-01-01
  cron: '@daily'
```

This matches the Postgres setup in [LDK setup](/build/get-started/prerequisites/ldk-setup.md). If you ran a warehouse on a different host or port, change `host` and `port` to match.

</details>

Confirm Vulcan can reach the engine:

```bash
vulcan info
```

A line reading `Data warehouse connection succeeded` means you are ready. Do not continue until you see it.

## 3. Add the source data

Place three CSV files under `seeds/`. They are the source of truth for this product. Keep dates in January 2025 so they align with `model_defaults.start`.

| File                  | Holds                        | Key columns                                                   |
| --------------------- | ---------------------------- | ------------------------------------------------------------- |
| `raw_customers.csv`   | Customer identity and region | `customer_id`, `name`, `email`, `region`                      |
| `raw_orders.csv`      | Order headers                | `order_id`, `customer_id`, `order_date`                       |
| `raw_order_items.csv` | Line-level detail            | `order_id`, `item_id`, `product_id`, `quantity`, `unit_price` |

Use this sample data to start. It spans 4 regions and satisfies the assertions you add next. Copy each block into the matching file under `seeds/`.

<details>

<summary><code>seeds/raw_customers.csv</code></summary>

```csv
customer_id,name,email,region
1,Ava Stone,ava@example.com,North
2,Ben Cole,ben@example.com,South
3,Cara Diaz,cara@example.com,East
4,Dan Frost,dan@example.com,West
```

</details>

<details>

<summary><code>seeds/raw_orders.csv</code></summary>

```csv
order_id,customer_id,order_date
101,1,2025-01-03
102,2,2025-01-03
103,3,2025-01-04
104,4,2025-01-05
105,1,2025-01-06
```

</details>

<details>

<summary><code>seeds/raw_order_items.csv</code></summary>

```csv
order_id,item_id,product_id,quantity,unit_price
101,1,SKU-1,2,19.99
101,2,SKU-2,1,5.00
102,1,SKU-1,3,19.99
103,1,SKU-3,1,49.50
104,1,SKU-2,5,5.00
105,1,SKU-3,2,49.50
```

</details>

## 4. Load the seeds as models

A `SEED` model loads a CSV into the engine. Create one `.sql` file per table under `models/`. Each declares its columns, its grain, and **assertions** that block bad rows when the model materializes.

<details>

<summary><code>models/raw_customers.sql</code></summary>

```sql
MODEL (
  name workspace.raw_customers,
  kind SEED (path '../seeds/raw_customers.csv'),
  columns (customer_id INTEGER, name TEXT, email TEXT, region TEXT),
  grains (customer_id),
  assertions (
    not_null(columns := (customer_id, name, email, region)),
  ),
);
```

</details>

<details>

<summary><code>models/raw_orders.sql</code></summary>

```sql
MODEL (
  name workspace.raw_orders,
  kind SEED (path '../seeds/raw_orders.csv'),
  columns (order_id INTEGER, customer_id INTEGER, order_date DATE),
  grains (order_id),
  references customer_id,
  assertions (
    not_null(columns := (order_id, customer_id, order_date)),
  ),
);
```

</details>

<details>

<summary><code>models/raw_order_items.sql</code></summary>

```sql
MODEL (
  name workspace.raw_order_items,
  kind SEED (path '../seeds/raw_order_items.csv'),
  columns (
    order_id INTEGER, item_id INTEGER, product_id TEXT,
    quantity INTEGER, unit_price DOUBLE PRECISION
  ),
  grains (order_id, item_id),
  references order_id,
  assertions (
    not_null(columns := (order_id, item_id, product_id, quantity, unit_price)),
    forall(criteria := (quantity > 0)),
    forall(criteria := (unit_price >= 0)),
  ),
);
```

</details>

Assertions are your first line of defense: a null key or a negative price stops the model from materializing. See [Assertions](/build/productize/assertions.md) for how they work.

## 5. Transform

Join the three seeds into one analytics-ready view. Create `models/order_lines_enriched.sql`:

<details>

<summary><code>models/order_lines_enriched.sql</code></summary>

```sql
MODEL (
  name workspace.order_lines_enriched,
  kind VIEW,
  grains (order_id, item_id),
  assertions (
    not_null(columns := (order_id, item_id, customer_id, order_date, region, line_revenue)),
    forall(criteria := (line_revenue >= 0)),
  ),
);

SELECT
  i.order_id, i.item_id, i.product_id, i.quantity, i.unit_price,
  (i.quantity * i.unit_price)::DOUBLE PRECISION AS line_revenue,
  o.customer_id, o.order_date,
  c.name AS customer_name, c.email AS customer_email, c.region
FROM workspace.raw_order_items AS i
INNER JOIN workspace.raw_orders AS o ON i.order_id = o.order_id
INNER JOIN workspace.raw_customers AS c ON o.customer_id = c.customer_id;
```

</details>

## 6. Define the semantic model and metric

The semantic model maps physical columns to business concepts: dimensions to group by and measures to aggregate. It powers REST, GraphQL, and MySQL wire-protocol APIs automatically. You write no API code.

Wrap the transform from step 5 in one semantic model under `models/semantics/`. It exposes `region` and `order_date` as dimensions and `line_revenue` as a summable measure named `total_line_revenue`:

<details>

<summary><code>models/semantics/order_lines.yml</code></summary>

```yaml
kind: semantic
name: order_lines
depends_on: workspace.order_lines_enriched
description: Order lines enriched with customer region and line revenue.
dimensions:
  - order_date
  - region
measures:
  - name: total_line_revenue
    type: sum
    expression: "{order_lines.line_revenue}"
    description: Sum of line-level revenue.
```

</details>

Then define the metric on top of it. The metric references the semantic model's measure and dimensions, not raw columns:

<details>

<summary><code>models/metrics/daily_revenue_by_region.yml</code></summary>

```yaml
kind: metric
name: daily_revenue_by_region
measure: order_lines.total_line_revenue
ts: order_lines.order_date
granularity: day
dimensions:
  - order_lines.region
description: Daily gross revenue from order lines, broken out by customer region.
```

</details>

One metric, one definition. Every dashboard, report, and AI agent now reads the same number. See [Semantic model](/build/productize/assets/semantic-model.md) for the full shape, including `ai_context` that makes the metric safe for an agent to query.

## 7. Plan, run, and check

```bash
vulcan plan
```

Vulcan validates the models and computes what to materialize. Review the plan: you should see the three seed models, the view, the semantic model, and the metric. When prompted `Apply the plan? [y/n]`, type `y`.

Then run the quality checks:

```bash
vulcan audit
```

All assertions and checks on the three models should pass. If one fails, Vulcan prints sample rows. Fix the seed data and run again.

## What you just built

A complete Data Product on your machine: raw inputs, a tested transform, quality gates that block bad data, a semantic model, and one defined metric, ready to serve over REST, GraphQL, and MySQL wire-protocol APIs the moment you deploy. You never wrote API code and you never deployed to the cloud.

## Next

* Build the real thing with the full worked example in [Productize](/build/productize/overview.md).
* Let an agent generate the files for you: [Build journey with AI](/build/get-started/build-journey-with-ai.md).
* Set up a real engine and Git deployment in [Prerequisites](/build/get-started/prerequisites.md), then [deploy](/build/productize/git-and-deploy.md).


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://v2.dataos.info/build/get-started/quickstart.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
