> 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/references/dataos-resources/vulcan/core-concepts/model-and-kinds.md).

# Model and kinds

Models transform raw data into tables and views. You define what you want (the metadata) and how to make it (the query), and Vulcan handles the rest. The model is the center of gravity in a Vulcan project: tests, audits, DQ checks, semantics, macros, and signals all attach to a model.

Models live in `.sql` and `.py` files in the `models/` directory. Vulcan figures out how models relate to each other by parsing your SQL, so you do not configure dependencies by hand for SQL models. Write your SQL, and Vulcan builds the lineage.

Every model has two parts:

* **The `MODEL` block (DDL).** Tells Vulcan what this model is: name, schedule, how to materialize it, and metadata.
* **The query (DML).** The `SELECT` (or Python function) that does the transformation.

## Model structure

You can write models in SQL or Python. Both do the same thing conceptually.

{% tabs %}
{% tab title="SQL" %}

```sql
MODEL (
  name sales.daily_sales,
  kind FULL,
  cron '@daily',
  grains (order_date),
  tags ('silver', 'sales', 'aggregation'),
  description 'Daily sales summary with order counts and revenue',
  column_descriptions (
    order_date = 'Date of the sales transactions',
    total_orders = 'Total number of orders for the day',
    total_revenue = 'Total revenue for the day'
  )
);

SELECT
  CAST(order_date AS TIMESTAMP)::TIMESTAMP AS order_date,
  COUNT(order_id)::INTEGER AS total_orders,
  SUM(total_amount)::FLOAT AS total_revenue,
  MAX(order_id)::VARCHAR AS last_order_id
FROM raw.raw_orders
GROUP BY order_date
ORDER BY order_date
```

{% endtab %}

{% tab title="Python" %}

```python
import typing as t
import pandas as pd
from datetime import datetime
from vulcan import ExecutionContext, model
from vulcan import ModelKindName

@model(
  "sales.daily_sales_py",
  columns={
    "order_date": "timestamp",
    "total_orders": "int",
    "total_revenue": "decimal(18,2)",
    "last_order_id": "string",
  },
  kind=dict(name=ModelKindName.FULL),
  grains=["order_date"],
  depends_on=["raw.raw_orders"],
  cron='@daily',
  tags=["silver", "sales", "aggregation"],
  description="Daily sales summary with order counts and revenue",
)
def execute(
  context: ExecutionContext,
  start: datetime,
  end: datetime,
  execution_time: datetime,
  **kwargs: t.Any,
) -> pd.DataFrame:
  query = """
  SELECT
    CAST(order_date AS TIMESTAMP) AS order_date,
    COUNT(order_id)::INTEGER AS total_orders,
    SUM(total_amount)::NUMERIC(18,2) AS total_revenue,
    MAX(order_id)::VARCHAR AS last_order_id
  FROM raw.raw_orders
  GROUP BY order_date
  ORDER BY order_date
  """
  return context.fetchdf(query)
```

{% endtab %}
{% endtabs %}

## SQL conventions

Vulcan infers as much as possible from your SQL, so you rarely write extra config. To make that work, your SQL follows a few conventions:

* **Unique column names.** Your final `SELECT` needs unique column names. Duplicates confuse lineage.
* **Explicit types.** Cast types explicitly (`COUNT(order_id)::INTEGER`). Vulcan uses PostgreSQL-style casts (`x::int`) and converts them to your engine's syntax. Without explicit casts you can get surprising types.
* **Inferrable names.** Columns need names Vulcan can figure out. Add an alias to anything that is not a bare column reference (`SUM(total_amount) AS revenue`).

Python models work differently: you must define `columns` (Vulcan cannot infer types from Python) and `depends_on` (Vulcan cannot parse Python for `FROM` clauses), and the `execute` function must return a DataFrame whose columns match `columns`. The DataFrame may be Pandas, PySpark, Snowpark, or Bigframe. For large output, use a generator (`yield`) to return the data in batches. Returning an empty DataFrame errors; `yield` instead (for example `yield from ()` when there are no rows). Python models do not support the `VIEW`, `SEED`, `MANAGED`, or `EMBEDDED` kinds. If a Python model needs extra packages, build them as wheels and place them under `dependencies/python/`.

## Common properties

The `MODEL` block accepts many properties. The ones you use most often:

| Property         | What it does                                                                                                                                         | Example                                     |
| ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------- |
| `name`           | Fully qualified name: `schema.model` or `catalog.schema.model`. Required unless name inference is enabled.                                           | `sales.daily_sales`                         |
| `project`        | For multi-repo deployments; overrides the default project name for this model's physical tables.                                                     | `'analytics_project'`                       |
| `kind`           | Materialization strategy.                                                                                                                            | `FULL`, `INCREMENTAL_BY_TIME_RANGE`, `VIEW` |
| `cron`           | When to run.                                                                                                                                         | `'@daily'`, `'0 6 * * *'`                   |
| `cron_tz`        | Timezone for the cron schedule (affects when it runs, not interval math).                                                                            | `'America/Los_Angeles'`                     |
| `interval_unit`  | Granularity of data intervals (inferred from `cron` by default). Values: `year`, `month`, `day`, `hour`, `half_hour`, `quarter_hour`, `five_minute`. | `'hour'`                                    |
| `grains`         | Columns that make each row unique (the primary key).                                                                                                 | `(order_date)`, `(customer_id, order_date)` |
| `references`     | Non-unique join relationship columns (e.g. `guest_id AS account_id`). Distinct from `grains`, which are unique identifiers.                          | `(customer_id, guest_id AS account_id)`     |
| `owner`          | Who owns the model, for governance and notifications.                                                                                                | `'analytics_team'`                          |
| `description`    | Registered as a table comment where supported.                                                                                                       | `'Daily sales aggregates'`                  |
| `tags` / `terms` | Labels and business glossary terms for organization and discovery.                                                                                   | `('gold', 'sales')`                         |
| `start` / `end`  | Earliest and latest date to process (absolute, relative, or epoch ms).                                                                               | `'2024-01-01'`, `'1 year ago'`              |
| `dialect`        | SQL dialect of the model (defaults to `model_defaults`).                                                                                             | `'snowflake'`                               |
| `assertions`     | Audits attached to this model (see [Assertions](/references/dataos-resources/vulcan/core-concepts/assertions.md)).                                   | `not_null(columns := (id))`                 |
| `depends_on`     | Explicit dependencies, added to those Vulcan infers. Required for Python models.                                                                     | `['sales.products']`                        |

Column-level metadata (`column_descriptions`, `column_tags`, `column_terms`) documents and classifies columns. Use the `pii` tag on personally identifiable information for governance. DDL descriptions take priority over inline comments; Vulcan registers comments on physical tables and production views where the engine supports it (Postgres, Snowflake, Spark).

### Storage and engine properties

Engine-specific properties control how data is physically stored. They are supported per engine:

* `partitioned_by` and `clustered_by` speed up queries through partition pruning and clustering (Spark, BigQuery, Databricks, and others).
* `table_format` (`iceberg`, `hive`, `delta`) and `storage_format` (`parquet`, `orc`) select formats.
* `physical_properties`, `virtual_properties`, and `session_properties` pass engine-specific settings to the physical table, the virtual-layer view, and the session.
* `gateway` routes a model to a specific gateway.

### Behavior properties

* `stamp` forces a new version without changing the definition.
* `enabled` (default `true`) turns a model off without deleting it.
* `allow_partials` (default `false`) lets a model process incomplete intervals.
* `optimize_query` (default `true`), `formatting` (default `true`), and `ignored_rules` control the optimizer, formatter, and linter for a model.

## Statements

Statements run SQL at specific points during model execution. Use them to set session parameters, load UDFs, create indexes, or grant permissions.

* **Pre-statements** run before the main query. Keep them to session settings, UDFs, and temporary objects. Avoid creating or altering physical tables in pre-statements, because concurrent models can hit race conditions.
* **Post-statements** run after the main query. When you use them in a SQL model, the main query must end with a semicolon. Use them for indexes, clustering, validation, or logging.
* **On-virtual-update statements** run when the virtual-layer view is created or updated. Use them for `GRANT` and access control. At this layer, table names resolve to view names.

Define statements at the model level, or project-wide through `model_defaults`:

```sql
MODEL (
  name analytics.orders,
  kind INCREMENTAL_BY_TIME_RANGE (time_column order_date)
);

SELECT order_id, order_date, customer_id, total_amount
FROM demo.raw_data.orders
WHERE order_date BETWEEN @start_date AND @end_date;

/* Post-statement: conditional retention only on table creation */
@IF(@runtime_stage = 'creating', ALTER TABLE @this_model SET DATA_RETENTION_TIME_IN_DAYS = 30);

/* On-virtual-update: grant on the view */
ON_VIRTUAL_UPDATE_BEGIN;
JINJA_STATEMENT_BEGIN;
GRANT SELECT ON VIEW {{ this_model }} TO ROLE view_only_role;
JINJA_END;
ON_VIRTUAL_UPDATE_END;
```

Useful macros in statements: `@this_model` (the current model's table or view), `@runtime_stage` (`'creating'`, `'evaluating'`, `'testing'`, and others), `@IF(condition, statement)`, and `@start_date` / `@end_date`. See [Macros](/references/dataos-resources/vulcan/core-concepts/macros.md).

{% hint style="warning" %}
Pre- and post-statements are evaluated at both the creation and evaluation stages, so they run twice. When a statement should run only once, guard it with `@IF` and `@runtime_stage`, for example `@IF(@runtime_stage = 'creating', ...)`.
{% endhint %}

## Model kinds

The `kind` property determines how Vulcan computes and stores a model. `VIEW` is the default for SQL; `FULL` is the default for Python.

| Kind                                          | Behavior                                                                                | Use it for                                                                                            |
| --------------------------------------------- | --------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
| `VIEW`                                        | A virtual table; the query runs on every reference. No data stored.                     | Simple, fast, always-fresh transformations.                                                           |
| `FULL`                                        | Rebuilds the whole table each run.                                                      | Small datasets, aggregates with no time dimension.                                                    |
| `INCREMENTAL_BY_TIME_RANGE`                   | Processes only the time intervals that are missing or changed.                          | Time-series data: events, logs, transactions.                                                         |
| `INCREMENTAL_BY_UNIQUE_KEY`                   | Upserts on a key (insert new, update existing).                                         | Dimension tables, customer records.                                                                   |
| `INCREMENTAL_BY_PARTITION`                    | Processes by partition; uses `partitioned_by` as the key.                               | Partition-oriented data.                                                                              |
| `INCREMENTAL_UNMANAGED`                       | Append-only; runs the query and appends the results with no dedup, updates, or deletes. | Data Vault hubs/links/satellites, event logs, audit trails.                                           |
| `SCD_TYPE_2_BY_TIME` / `SCD_TYPE_2_BY_COLUMN` | Tracks historical changes with `valid_from` / `valid_to`.                               | Slowly changing dimensions.                                                                           |
| `EMBEDDED`                                    | A reusable SQL snippet injected into downstream models as a subquery. No table or view. | Shared CTEs and reusable logic.                                                                       |
| `SEED`                                        | Loads a static CSV into a table.                                                        | Reference and lookup data.                                                                            |
| `CUSTOM`                                      | Your own materialization in Python.                                                     | When no built-in kind fits (see [Optimisation](/references/dataos-resources/vulcan/optimisation.md)). |

### Incremental by time range

`INCREMENTAL_BY_TIME_RANGE` fits time-series data. Tell Vulcan the time column and add a `WHERE` clause that filters upstream data by the processing window using time macros:

```sql
MODEL (
  name vulcan_demo.daily_sales,
  kind INCREMENTAL_BY_TIME_RANGE (
    time_column order_date
  ),
  start '2025-01-01',
  cron '@daily',
  grains (order_date, product_id)
);

SELECT
  o.order_date,
  p.product_id,
  SUM(oi.quantity * oi.unit_price) AS total_sales_amount
FROM vulcan_demo.orders AS o
JOIN vulcan_demo.order_items AS oi ON o.order_id = oi.order_id
JOIN vulcan_demo.products AS p ON oi.product_id = p.product_id
WHERE o.order_date BETWEEN @start_ds AND @end_ds
GROUP BY o.order_date, p.product_id
```

Two `WHERE` filters work together: yours filters the input data for performance, and Vulcan automatically adds an output filter on the time column to prevent data leakage even if your `WHERE` clause has a bug. When your `WHERE` clause filters on a different column than `time_column` (for example, filtering on `shipped_date` while `time_column` is `order_date`), Vulcan appends an extra filter on `time_column` at runtime to prevent rows from adjacent intervals leaking into the current interval.

Specify a format for the time column inline as a tuple when the column is not in the default `%Y-%m-%d` format:

```sql
kind INCREMENTAL_BY_TIME_RANGE (
  time_column (order_date, '%Y-%m-%d')
)
```

By default Vulcan adds the time column to the partition key. Common kind properties include `format`, `batch_size`, `batch_concurrency`, `lookback` (for late-arriving data), and `partition_by_time_column false`.

{% hint style="info" %}
Your `time_column` should be in UTC. The `cron_tz` flag only changes when a model runs, not how intervals are calculated. See the [incremental by time guide](/references/dataos-resources/vulcan/incremental-by-time.md) for a worked example.
{% endhint %}

### Incremental by unique key

`INCREMENTAL_BY_UNIQUE_KEY` upserts on a key: insert a new key, update an existing key, leave keys not present in new data alone. Provide the key, optionally a composite key or expression:

```sql
MODEL (
  name vulcan_demo.customers,
  kind INCREMENTAL_BY_UNIQUE_KEY (
    unique_key customer_id,
    when_matched (
      WHEN MATCHED THEN UPDATE SET target.email = COALESCE(source.email, target.email)
    ),
    merge_filter target.order_date > dateadd(day, -7, current_date)
  )
);
```

`when_matched` customizes which columns update on a match (use `source` and `target` aliases). `merge_filter` limits the MERGE scan on large tables. These models are non-idempotent, so partial restatement is not supported: a restatement recreates the table. `when_matched` works only on engines that support `MERGE` (BigQuery, Databricks, Postgres, Redshift with `enable_merge: true`, Snowflake, Spark). Note that `INCREMENTAL_BY_UNIQUE_KEY` is not supported on Spark.

The unique key can be a SQL expression rather than a bare column name:

```sql
kind INCREMENTAL_BY_UNIQUE_KEY (
  unique_key COALESCE("email", '')
)
```

{% hint style="info" %}
**Redshift**: set `enable_merge: true` in your gateway connection config to use MERGE. Redshift supports only a single `WHEN MATCHED` clause and does not allow both UPDATE and DELETE in the same MERGE statement.

**Batch concurrency**: `batch_concurrency` is not supported for `INCREMENTAL_BY_UNIQUE_KEY` because MERGE operations cannot safely run in parallel.
{% endhint %}

### Incremental by partition

`INCREMENTAL_BY_PARTITION` loads and updates rows as a group based on their `partitioned_by` key. When new data arrives:

* A partition key not yet in the table is inserted.
* A partition key already in the table has **all its existing rows replaced** with the newly loaded rows for that key.
* A partition key present in the table but absent from the new data is left untouched.

{% hint style="warning" %}
These models are non-idempotent, so partial restatement is not supported and restating recreates the whole table. Because a key absent from new data is left in place, a restatement can leave stale rows behind, so there is a data-loss risk. Restate with care.
{% endhint %}

### Incremental unmanaged

`INCREMENTAL_UNMANAGED` is append-only: each run executes your query and appends the results, with no deduplication, updates, or deletes. Use it for true append-only patterns such as Data Vault hubs, links, and satellites, event logs, and audit trails; otherwise `INCREMENTAL_BY_TIME_RANGE` or `INCREMENTAL_BY_UNIQUE_KEY` give you more control.

```sql
MODEL (
  name vulcan_demo.incremental_unmanaged,
  kind INCREMENTAL_UNMANAGED,
  cron '@daily',
  start '2025-01-01',
  grains (shipment_id)
);
```

Because it is unmanaged, this kind does not support `batch_size` or `batch_concurrency`, and only full restatements are supported (the model is rebuilt from scratch, not from a time slice you specify).

### Schema changes and restatement

These properties go inside the `kind` definition and work with every incremental kind. They control how Vulcan handles schema evolution and restatement:

| Property                     | What it does                                                                     | Default |
| ---------------------------- | -------------------------------------------------------------------------------- | ------- |
| `forward_only`               | Treat all changes as forward-only (no backfill of the new version).              | `false` |
| `on_destructive_change`      | Behavior for destructive schema changes: `allow`, `warn`, `error`, or `ignore`.  | `error` |
| `on_additive_change`         | Behavior for additive schema changes: `allow`, `warn`, `error`, or `ignore`.     | `allow` |
| `disable_restatement`        | Disable data restatement for the model.                                          | `false` |
| `auto_restatement_cron`      | Cron expression that automatically restates the model.                           | -       |
| `auto_restatement_intervals` | Number of trailing intervals to auto-restate (`INCREMENTAL_BY_TIME_RANGE` only). | -       |

### SCD Type 2

SCD Type 2 tracks history. Vulcan adds `valid_from` (inclusive) and `valid_to` (exclusive, NULL for the latest row). Use `SCD_TYPE_2_BY_TIME` (recommended) when your source has an `updated_at` column, and `SCD_TYPE_2_BY_COLUMN` when it does not. Partial restatement is not supported, so restatement is disabled by default for this kind.

```sql
MODEL (
  name dim.customers,
  kind SCD_TYPE_2_BY_TIME (
    unique_key customer_id,
    updated_at_name last_modified
  )
);
```

SCD Type 2 kinds accept these additional properties:

| Property                            | What it does                                                                                                                                                                                 | Default                   |
| ----------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------- |
| `invalidate_hard_deletes`           | When a record disappears from the source, set `valid_to` to the detection time (a hard delete). With `false` (default), missing records keep `valid_to` NULL (still valid, no history gaps). | `false`                   |
| `updated_at_as_valid_from`          | (By-time) Set new rows' `valid_from` to their `updated_at` value instead of `1970-01-01 00:00:00`.                                                                                           | `false`                   |
| `updated_at_name`                   | (By-column only) For snapshot-table sources that already have a timestamp column to use as `valid_from`; set this to that column's name.                                                     | `null`                    |
| `execution_time_as_valid_from`      | (By-column) Always use the execution time as `valid_from` for new rows.                                                                                                                      | `false`                   |
| `valid_from_name` / `valid_to_name` | Rename the generated validity columns.                                                                                                                                                       | `valid_from` / `valid_to` |
| `time_data_type`                    | Data type for the validity columns (for example `TIMESTAMP` instead of BigQuery's default `DATETIME`).                                                                                       | engine default            |
| `batch_size`                        | When processing a source table with historical data, set to `1` to process intervals sequentially (one at a time per cron interval).                                                         | `null`                    |

#### Querying SCD Type 2 models

| Pattern                         | SQL                                                                                  |
| ------------------------------- | ------------------------------------------------------------------------------------ |
| Current records                 | `WHERE valid_to IS NULL`                                                             |
| Point-in-time (e.g. 2024-06-01) | `WHERE valid_from <= '2024-06-01' AND (valid_to IS NULL OR valid_to > '2024-06-01')` |
| Hard-deleted records            | `WHERE valid_to IS NOT NULL`                                                         |
| Make `valid_to` inclusive       | `COALESCE(valid_to, TIMESTAMP '9999-12-31') - INTERVAL 1 SECOND`                     |

{% hint style="info" %}
To reset SCD history: (1) temporarily set `disable_restatement: false`, (2) run `vulcan plan <env>` and apply, (3) run `vulcan plan <env> --restate <model>` and apply, (4) re-enable `disable_restatement: true`.
{% endhint %}

### Seed

`SEED` loads a static CSV. Point to the file and define the schema:

```sql
MODEL (
  name vulcan_demo.seed_model,
  kind SEED (path '../seeds/seed_data.csv'),
  columns (id INT, item_id INT, event_date DATE),
  grains (id)
);
```

Seeds reload only when you change the model definition or the CSV.

### Materialized views

Turn a `VIEW` into a materialized view with `materialized true` (supported on BigQuery, Databricks, Snowflake; ignored elsewhere). For engine-driven automatic refresh, see [managed models](#managed-models).

```sql
MODEL (
  name vulcan_demo.sales_summary,
  kind VIEW (materialized true)
);
```

### Materialization strategy by engine

Each kind materializes differently per engine. For example, `FULL` runs `CREATE OR REPLACE TABLE` on Snowflake, BigQuery, and DuckDB, `INSERT OVERWRITE` on Spark and Databricks, and `DROP`/`CREATE`/`INSERT` on Redshift and Postgres. `INCREMENTAL_BY_TIME_RANGE` uses `INSERT OVERWRITE` by partition on Spark and Databricks, and `DELETE` by time range then `INSERT` on Snowflake, BigQuery, Redshift, Postgres, and DuckDB. Vulcan picks the right strategy for your gateway.

## Model types

Beyond SQL and Python, two model types describe or delegate data Vulcan does not fully own:

| Type       | Managed by Vulcan | Query required | Best for                                               |
| ---------- | :---------------: | :------------: | ------------------------------------------------------ |
| SQL        |        Yes        |       Yes      | Standard transformations and warehouse-native logic.   |
| Python     |        Yes        |  No (function) | API calls, machine learning, complex procedural logic. |
| `EXTERNAL` |         No        |       No       | Schema metadata for tables outside the project.        |
| `MANAGED`  |       Partly      |       Yes      | Engine-driven refresh on supported engines.            |

Start with SQL for most projects. Add Python when SQL is not enough.

### Python models — Context API

The `ExecutionContext` object passed to `execute` exposes these methods and attributes:

| Method / attribute                    | What it does                                                                                   |
| ------------------------------------- | ---------------------------------------------------------------------------------------------- |
| `context.var("name", default?)`       | Access a global variable; optional default returned when the variable is not set.              |
| `context.resolve_table("model.name")` | Returns the physical table name for the current environment and auto-registers the dependency. |
| `context.spark`                       | PySpark `SparkSession` for PySpark models.                                                     |
| `context.snowpark`                    | Snowpark `Session` for Snowpark models.                                                        |
| `context.bigframe`                    | BigFrame session for BigQuery BigFrame models.                                                 |
| `context.engine_adapter.execute(sql)` | Run SQL directly; use with `yield` for post-statement sequencing.                              |

`context.resolve_table()` auto-registers the referenced model as a dependency. `depends_on` declared in the `@model` decorator takes precedence over any dynamic references discovered via `resolve_table` calls inside `execute`.

#### `yield` for post-statement sequencing

When a post-statement must run after the model's main output is written, use `yield` instead of `return`:

```python
def execute(context: ExecutionContext, **kwargs):
    yield df  # write the DataFrame first
    context.engine_adapter.execute("GRANT SELECT ON ...")  # then run the post-statement
```

#### Macros in cron expressions

When a Python model's cron expression contains macro variables, wrap the whole expression with `@'...'` to avoid conflict with the `@` macro prefix:

```python
@model(
    "schema.scheduled_model",
    cron="@'*/@{mins} * * * *'",  # @'...' prevents the macro parser from consuming @{mins} early
)
```

### External models

External models are metadata-only descriptions of tables outside your project (a third-party source, a read-only database, a table another system manages). Vulcan does not run a query for them and never modifies them. Declaring their schema gives you column-level lineage, query optimization, and catalog documentation.

Define external models in `external_models.yaml` (or split across `external_models/`), or generate them:

```bash
vulcan create_external_models
```

This scans your project for external table references, fetches column metadata from the engine, and writes the file. It only reads metadata, never the data.

{% hint style="warning" %}
`vulcan create_external_models` overwrites `external_models.yaml` on each run. To prevent manual entries from being lost, put hand-written definitions in an `external_models/` directory instead — Vulcan loads and merges files from both locations at plan time.

Inline `audits:` in `external_models.yaml` is no longer supported. To validate upstream data from an external source, write standalone audits in `audits/*.sql` and attach them as assertions on downstream models, or add a DQ check in `dq/*.yml`.
{% endhint %}

### Managed models

Managed models let the engine refresh data automatically in the background instead of Vulcan running the refresh. They are typically built on external models, because Vulcan already keeps its own models fresh. Python models do not support `MANAGED`.

On Snowflake, managed models are implemented as Dynamic Tables. Set engine properties through `physical_properties`:

```sql
MODEL (
  name db.events,
  kind MANAGED,
  physical_properties (
    warehouse = datalake,
    target_lag = '2 minutes',
    data_retention_time_in_days = 2
  )
);

SELECT
  event_date::DATE AS event_date,
  event_payload::TEXT AS payload
FROM raw_events
```

Vulcan does not create intervals or schedule these models, so you do not need a date `WHERE` clause. To control cost, Vulcan uses regular tables for dev previews and creates managed tables only when deploying to production, so a query that works in dev can fail in prod if it relies on a feature managed tables do not support.

On Snowflake, `target_lag` is the only required property. If `warehouse` is omitted, Vulcan uses the result of `select current_warehouse()`.

{% hint style="warning" %}
Managed models are still under active development; the API and semantics may change as more engines are added.
{% endhint %}

## Blueprinting

Blueprinting generates N model instances from a single definition. Add a `blueprints` property to the `MODEL` block (SQL) or the `@model` decorator (Python); Vulcan creates one model per entry. Use `@{variable}` in the model name — the curly braces render the value as a SQL identifier.

{% tabs %}
{% tab title="SQL" %}

```sql
MODEL (
  name catalog.schema.@{category}_sales,
  kind FULL,
  blueprints (
    (category := 'electronics'),
    (category := 'clothing')
  )
);

SELECT
  @BLUEPRINT_VAR('category', 'unknown') AS product_category,
  SUM(revenue)                          AS total_revenue
FROM raw.sales
WHERE category = @category
GROUP BY 1
```

`@BLUEPRINT_VAR('name', default)` returns the current blueprint's value for `name` inside the model body. `@{category}` in the `name` renders as an identifier (no quotes); `@category` in the `WHERE` clause renders as a quoted string literal.
{% endtab %}

{% tab title="Python" %}

```python
@model(
    "schema.@{category}_sales",
    kind=dict(name=ModelKindName.FULL),
    columns={"product_category": "text", "total_revenue": "decimal(18,2)"},
    blueprints=[{"category": "electronics"}, {"category": "clothing"}],
)
def execute(context: ExecutionContext, **kwargs):
    cat = context.blueprint_var("category")
    return context.fetchdf(f"SELECT '{cat}' AS product_category, SUM(revenue) AS total_revenue FROM raw.sales WHERE category = '{cat}'")
```

Access the current blueprint value inside `execute` with `context.blueprint_var("variable_name")`.
{% endtab %}
{% endtabs %}

**Dynamic blueprints:** set `blueprints` to a macro call to derive the list at plan time rather than hardcoding it:

```sql
-- SQL
MODEL (
  name schema.@{category}_sales,
  blueprints @EACH(@category_list, x -> (category := @x))
);
```

```python
# Python
@model("schema.@{category}_sales", blueprints="@EACH(@category_list, x -> (category := schema_@x))", ...)
```

## Related docs

* [Incremental by time](/references/dataos-resources/vulcan/incremental-by-time.md): a worked example with backfill and forward-only models.
* [Assertions](/references/dataos-resources/vulcan/core-concepts/assertions.md): attach audits to models to block bad rows.
* [Macros](/references/dataos-resources/vulcan/core-concepts/macros.md): make model SQL dynamic with variables and functions.
* [Optimisation](/references/dataos-resources/vulcan/optimisation.md): custom materializations and performance tuning.


---

# 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/references/dataos-resources/vulcan/core-concepts/model-and-kinds.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.
