# YAML Format — File Specification

## Header Structure

Every Lumo-managed file begins with a universal YAML header:

```yaml
lumo: v2                    # Required: schema version
kind: flow                  # Required: resource type
id: 42866                   # 0 or absent = create new, > 0 = update
tenantId: 522               # Safety check (compared with current session)
---
# Resource body below (kind-specific)
nome: Carga Vendas
load_type: Temporal
```

## Header Fields

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `lumo` | string | Yes | Schema version. Current declarative workspace uses `v2`. |
| `kind` | string | Yes | Resource type (flow, table, app, credential, schedule, agent, variables, dw-desk, bi-desk). |
| `id` | number | No | Backend ID. Assigned by server on creation. |
| `tenantId` | number | No | Safety check against current workspace tenant. |

## Valid Kinds

```
flow, table, app, credential, schedule, agent, variables, dw-desk, bi-desk
```

`desk` (single) is still accepted as a legacy alias for `bi-desk`. Prefer the typed forms (`dw-desk` for data desks holding flows/tables, `bi-desk` for application desks holding apps/dashboards). Admin kinds (`user`, `group`) are managed through the web UI, not the CLI.

## Separator

The `---` line separates the metadata header from the resource body. This dual-document structure allows the CLI to manage sync state (IDs, hashes) without touching your configuration logic.

## Resource Body (Editable)

The body below `---` is what you edit. Different kinds have different body schemas:

- **Flows**: nodes (Extract, SQLProcessor, PythonProcessor, InsertDatawarehouse), load_type, tokenId.
- **Apps**: dashboards, widgets, expressions, relationships, facts.
- **Tables**: labels, masks, descriptions, behaviors. (Note: structural columns are owned by the producing flow).
- **Credentials**: host, port, user, password, tokenId.

## Write-Back on Creation

When you run `lumo push` for a newly created resource (id was 0), the CLI updates the YAML header with the server-assigned `id` and `tenantId`. The body remains untouched.

```yaml
# BEFORE push:
lumo: v2
kind: flow
id: 0
---
nome: New Flow

# AFTER push:
lumo: v2
kind: flow
id: 42871              # ← Updated by CLI
tenantId: 522          # ← Updated by CLI
---
nome: New Flow         # ← UNTOUCHED
```

## Split Directory Mode

Complex resources like flows and apps can be managed as split directories. This is the preferred mode for large projects as it enables better diffs and git tracking.

### Flow Split
```
flows/carga-vendas--42866.yaml/ (or .dir/)
├── _flow.yaml               # Header + metadata (no inline nodes)
├── _graph.yaml              # Connections between nodes
└── nodes/
    ├── extract-pg--Chk2esZG.yaml
    ├── transform-duckdb--aB3cD4eF.yaml
    └── load-dw--xY7mZkLp.yaml
```

### App Split
```
apps/dashboard-vendas--16041.yaml/
├── _app.yaml                # Header + metadata
├── _tables.yaml             # Tables + relationships
├── _expressions.yaml        # Calculated metrics
└── dashboards/
    └── overview--5.yaml     # Dashboard metadata + inline widgets
```

## Common YAML Gotchas

### Unknown keys are NOT caught by lint
`lumo lint` validates against a JSON Schema that does **not** set
`additionalProperties: false`, so an unrecognized key passes silently — verified
with an invented key at both the document root and inside a widget: `lint`
returns clean. A typo like `dataSouce:` will not be flagged; it just does
nothing on push.

What `lint` **does** catch: missing required properties, wrong types, and values
outside a declared enum. Treat it as a floor, not a spell-checker. For field
names, copy from `lumo scaffold <subject>` instead of typing from memory, and
confirm the result with `lumo pull` after pushing.

### structural columns in Table YAML
Editing `column_name`, `data_type`, or `display_order` in a table YAML will be silently ignored/reverted on `push`. These fields are owned by the `InsertDatawarehouse` node of the flow that produces the table.

### Qualified Names and Data Sources (Apps)
When building BI dashboards, use `lumo info app:<id> --full` to get the correct identifiers:
- `qualifiedName` (e.g. `[Table]."COL"`): use inside formula expressions.
- `dataSource` (e.g. `12345:SUM`): use inside widget chart configurations.

### Python Node specific
- The output variable in a PythonProcessor must be named `df`.
- The source code field is `Sourcecode` (capital S, lowercase c).

### Node Input References
In SQLProcessor nodes, reference inputs as `input_0`, `input_1`, etc., based on connection order.

### Table Type Enum
`tables[].tipo` accepts only `"dimension"` (default) or `"n2n"`. `"dimension"` covers every normal app table — both fact tables and dimension tables. Do NOT write `"fact"`, `"dim"`, or `"bridge"`; the server rejects them. `"n2n"` is for many-to-many bridge tables only.

### Node internalId Uniqueness
Node `internalId` values must be unique across the **whole tenant**, not just inside their own flow. Two different flows each carrying a node called `ext01` collide. This surprises everyone, because nothing in the YAML suggests the id escapes its file: the agent keys per-node execution state (`processor:{internalId}` in Redis) by that string alone, with no flow in the key.

Scaffolds ship `ext01`, `py01`, `load01`. Rename them per flow — a short flow prefix works well (`titulo_ext01`, `titulo_load01`). Note also that `lumo flow clone` regenerates every internalId, so read the new ones with `lumo flow nodes flow:<clone-id>` before any `run-node`.

### Schedule Triggers Are Not Cron
Schedule triggers use `type: Day | Hour | Month | Minute | Week | CDC`. Do not write cron expressions. Required fields differ per type:

| type | Required | `start_when` format | `period` means |
|---|---|---|---|
| `Day` | `start_when` | `"07:00"` (HH:MM) | not used |
| `Month` | `period`, `start_when` | `"07:00"` (HH:MM) | day of month, 1-31 |
| `Week` | `period`, `start_when` | `"07:00"` (HH:MM) | day of week, 0=Sun..6=Sat |
| `Hour` | `period`, `start_when`, `start_time`, `end_time` | ISO-8601 (`"2026-01-01T07:00:00.000Z"`) | every N hours, 1-24 |
| `Minute` | `period`, `start_when`, `start_time`, `end_time` | ISO-8601 | every N minutes, 1-60 |
| `CDC` | `period`, `start_when`, `config` | ISO-8601, full anchor (no bare HH:MM) | probe interval in **seconds**, 5-3600 |

`Minute` additionally requires the tenant config `schedulingGranularity='minutes'`. `start_time` / `end_time` bound the daily window in which an `Hour` / `Minute` trigger is allowed to fire; they're optional on `CDC` for the same purpose.

`CDC` fires when a probe query's result changes instead of on a fixed interval. Its `config` block is a free map, but the backend requires `kind: query-scalar`, `credential` (an etl credential key, which does not have to match the credentials the schedule's flows use), and `sql` (must be a single statement starting with `SELECT` or `WITH`, returning exactly one row with 1 to 8 columns, up to 8192 bytes). With more than one column the probe fires when **any** of them changes. `SELECT COUNT(*), MAX(changed_at) FROM t` is the only rule that catches inserts, updates and deletes at once, and it takes two columns rather than one concatenated value because concatenation is dialect-specific (`||` on Oracle/Postgres, `+` on SQL Server, `CONCAT` on MySQL) and the target dialect is unknowable behind an ODBC DSN. Multi-column probes need agent **1.26.0** or newer; against an older agent the probe fails as a configuration error. `timeout_seconds` is optional (default 5) and must stay below `period`. Because of that default, `period: 5` with no explicit `timeout_seconds` is always rejected at push even though `5` satisfies the schema's minimum — the smallest `period` that works unmodified is effectively **6**; below that, set a `timeout_seconds` lower than `period` explicitly.

Any schedule, regardless of trigger type, can also set the top-level `parallel_lane: true` to run outside the agent's normal concurrent-execution slot limit. It's a property of the schedule, not of a trigger — use it only for lightweight schedules.

### Structural vs Editable Table Fields
- **Always editable**: `mask`, `label`, `defaultBehavior`, `header.nome`, `header.icon`, `header.human_description`.
- **Owned by flow (silently reverted)**: `column_name`, `data_type`, `display_order`, `is_key_column`, `is_partition_column`, `table_type`.
- On a **published** table, push attempts return `409 TABLE_READONLY_STRUCTURAL` with the list of fields you tried to change.
- To change structure: edit the `InsertDatawarehouse` node, `lumo flow run-node`, then `lumo fetch table:<id>`.
