Skip to content

writeback ​

Tipo
Referência
Para
Desenvolvedor, Engenharia de dados

Um writeback é um Cadastro: uma tabela cujas linhas as pessoas digitam num formulário da tela, sem Dataflow. O arquivo fica em writebacks/<slug>--<id>.yaml e guarda só o esquema (campos, tipos, seções, layout); as linhas nunca entram no YAML. O que é um Cadastro e quando usar está em Cadastros.

bash
lumo new writeback --name "Metas por Filial"
lumo new writeback --name "Metas por Filial" --from-file metas.xlsx   # cria e já carrega a planilha

Depois de editar, lumo push writeback:<id> e, uma vez, lumo writeback publish writeback:<id> --desk <dw-desk>.

Campos ​

  • fileName
    string · opcional · valores: regex ^[a-z0-9-]+--\d+\.yaml$
  • header
    object · obrigatório
  • header.id
    integer · obrigatório · valores: ≥ 1
  • header.kind
    string · obrigatório · valores: writeback (fixo)
  • header.lumo
    string · obrigatório · valores: v2 (fixo)
  • header.tenantId
    integer · obrigatório · valores: ≥ 1
  • header.appId
    integer · opcional · valores: ≥ 1
  • header.dashboardId
    integer · opcional · valores: ≥ 1
  • body
    object · obrigatório
  • body.name
    string · obrigatório · valores: não vazio
    Display name shown in the web UI's Cadastro list and as the form title. Purely cosmetic - renaming it does not touch the physical table or any ref pointing at this writeback.
  • body.alias
    string · obrigatório · valores: regex ^[A-Z][A-Z0-9_]{0,44}$
    Somente leitura: o servidor preenche. READ-ONLY. The physical Doris table name, generated by the server on create (name + id) and never changed afterward - it is what other writebacks' fields[].ref.table point at by string, so renaming it would silently orphan those references. lumo push rejects the whole request with a conflict (WB_ALIAS_READONLY) if this value differs from the server's stored alias; revert it to the value from the last lumo pull/fetch and push again. To rename the table shown to users, edit name instead - alias is the internal identifier, not the label.
  • body.deskId
    integer ou null · opcional · valores: ≥ 1
    Somente leitura: o servidor preenche. Server-managed publish state; change through publish commands, not declarative push.
  • body._state
    string · opcional · valores: draft, published, inconsistent
    Somente leitura: o servidor preenche. Derived lifecycle state. draft=deskId null (work-in-progress, owner-only). published=deskId set (live, readonly, shared). inconsistent=domain invariant violated (e.g. published flow ↔ draft table); treat as 'stop and ask'. Server-computed; edits are ignored on push.
  • body.sections
    array de string · opcional
    Ordered list of section headings the data-entry form groups fields under (see fields[].section). Cosmetic layout only - a field can reference a section name not listed here and the server accepts it; keeping this list in sync just controls the ORDER sections render in and whether an otherwise-unused heading still shows.
  • body.grid
    object · opcional
    Table-level grid display settings (distinct from each field's own grid block, which controls that one column's visibility/order/width).
  • body.grid.frozen_count
    integer · opcional · valores: ≥ 0
    Number of leading columns frozen (pinned) when the user scrolls the data-entry grid horizontally. 0 = none frozen.
  • body.hooks
    object · opcional
    Lifecycle hooks that run around row save.
  • body.hooks.on_save
    string ou null · opcional
    JavaScript run server-side (sandboxed) right before a row is saved; can read and mutate the record being written (e.g. record.ATUALIZADO_EM = new Date().toISOString();). null/omitted = no hook. This is arbitrary logic, not a cosmetic setting - changing it changes what gets persisted on every future save of this Cadastro.
  • body.unique_constraints
    array de array · opcional
    Row-level uniqueness restrictions, each a combination of field aliases enforced together (composite unique keys). [] means no uniqueness beyond the implicit row id.
  • body.fields
    array de object · obrigatório
    The editable columns of this Cadastro, in the order they render by default (see each field's own grid.order to reorder within that). May be empty ([]) for a Cadastro with no data columns yet. Every alias must be unique within this list - the schema cannot check that, the server does. Removing or retyping an existing field is destructive (drops the column's data on push); lumo push requires --rename/--allow-drop to acknowledge that explicitly.
  • body.fields[].alias
    string · obrigatório · valores: regex ^[A-Z][A-Z0-9_]{0,44}$
    The stored column name for this field (also what fields[].ref.key, unique_constraints[] entries and hook code reference it by). A-Z, 0-9, _ only, must start with a letter, max 45 chars. The H_ prefix is reserved for system columns and WB_ID is the reserved row-id column - both are rejected here. Renaming an existing field's alias is a destructive drop+add unless declared with lumo push --rename OLD=NEW; the schema cannot see that it is a rename versus a genuinely new field, only the server's diff against the stored definition can.
  • body.fields[].label
    string · obrigatório · valores: não vazio
    Human-readable caption shown next to this field in the data-entry form and as the column header in the BI layer. Purely cosmetic - safe to change any time.
  • body.fields[].type
    string · obrigatório · valores: number, date, string, external_key, image, picklist
    The field's data-entry widget and stored data type. number/string/image are self-contained. date additionally requires granularity. external_key additionally requires ref (a lookup into another table, rendered as a search/select in the form). picklist additionally requires a non-empty options list (rendered as a fixed dropdown). Changing type after the field has data can be destructive depending on whether the underlying physical data type changes (e.g. string→number) - the server classifies this at push time; the schema only enforces which companion keys each type requires.
  • body.fields[].mask
    string ou null · opcional
    Display/input mask for this field (e.g. "#,##0.00"), same convention as table column masks. null/omitted = no mask.
  • body.fields[].options
    array de object · obrigatório: sim, se type: picklist
    Only meaningful (and required, non-empty) when type: picklist - the fixed list of selectable values. Ignored for every other type; the server does not clear it if you leave it on a field you retype away from picklist, so remove it yourself when changing type to avoid a stale list.
  • body.fields[].options[].value
    string · obrigatório · valores: não vazio
    The exact string stored in the cell when this option is chosen. Must be unique within this field's options - the schema cannot check that, the server does.
  • body.fields[].options[].color
    string · opcional
    Free-form color for this option's chip/badge in the UI, e.g. a hex value like "#4E46DD". Optional - omit to use the default color.
  • body.fields[].tooltip
    string ou null · opcional
    Help text shown next to this field in the data-entry form. null/omitted = none. Purely cosmetic.
  • body.fields[].required
    boolean · opcional
    Whether the data-entry form requires the user to fill this field before saving a row. Defaults to false (optional) when omitted. This is a form-validation rule, unrelated to whether the key is required in THIS YAML.
  • body.fields[].default
    string ou number ou null · opcional
    Value pre-filled in the data-entry form for a new row. null/omitted = field starts empty. Must match the field's stored type (e.g. a number field takes a numeric default, not a numeric-looking string).
  • body.fields[].validation
    string ou null · opcional
    JavaScript run server-side (sandboxed) to validate this field's value on save; return a string to reject with that error message, or nothing/undefined to accept (e.g. if (value < 0) return "Meta não pode ser negativa";). null/omitted = no extra validation beyond the type/required checks.
  • body.fields[].granularity
    string ou null · obrigatório: sim, se type: date · valores: year, month, date, datetime
    Required when type: date - the precision the form captures and the column stores (year picker vs. full date vs. date+time, etc.). Ignored and meaningless for every other type.
  • body.fields[].ref
    object ou null · obrigatório: sim, se type: external_key
    Required when type: external_key - points this field at another table's key column, rendered as a lookup/search picker in the form. Ignored for every other type.
  • body.fields[].ref.table
    string · obrigatório · valores: não vazio
    Alias/name of the table being referenced, shown for readability. The server resolves the actual join by tableId, not by this string - if they disagree, tableId wins.
  • body.fields[].ref.tableId
    integer · obrigatório · valores: ≥ 1
    id of the referenced table. This is what the server actually joins on.
  • body.fields[].ref.key
    string · obrigatório · valores: não vazio
    Column name in the referenced table that this field's value is looked up/stored against (the join key).
  • body.fields[].ref.key_type
    string · opcional · valores: String, Number, Date
    Physical data type of key in the referenced table - determines the stored data type of this field itself. Defaults to "String" when omitted; set it explicitly whenever the referenced key is Number or Date, since a wrong value here mismatches the physical column type.
  • body.fields[].ref.label_expr
    string · obrigatório · valores: não vazio
    Column/expression from the referenced table shown to the user in the lookup picker and read-only display (e.g. a NOME_FILIAL column), so the form shows a human label instead of the raw key value.
  • body.fields[].section
    string ou null · opcional
    Name of the form section this field renders under - should match an entry in the top-level sections list, though the server does not enforce that match. null/omitted = ungrouped.
  • body.fields[].grid
    object · opcional
    Per-field grid display settings (distinct from the table-level grid block, which only controls frozen columns).
  • body.fields[].grid.visible
    boolean · opcional
    Whether this column shows in the data-entry grid/table view. Defaults to true when omitted. A hidden field is still editable via the row-detail form and still stores data - this only hides the grid column.
  • body.fields[].grid.order
    integer · opcional
    Sort position of this column in the data-entry grid, relative to the other fields' grid.order. Defaults to 0 when omitted.
  • body.fields[].grid.width
    integer ou null · opcional
    Column width in pixels in the data-entry grid. null/omitted = auto width.
  • body.fields[].grid.format
    string ou null · opcional
    Display format hint for this column in the grid (e.g. "currency"). null/omitted = default formatting for the field's type.
  • body.fields[].grid.align
    string ou null · opcional · valores: left, right, center
    Text alignment of this column in the data-entry grid. null/omitted = type-appropriate default (numbers typically right-aligned).
  • body.expressions
    array de object · opcional
    BI-only calculated columns layered on top of the stored fields (e.g. a SUM aggregation) - these are NOT physical columns of the writeback table, never appear in fields, and are not part of the data-entry form; they only affect how this Cadastro is consumed by dashboards/apps. column_name must not collide with WB_ID, with any fields[].alias, or with another expression's column_name - the schema can only reject the literal value WB_ID; the cross-checks against fields[] and against sibling expressions are server-side, at push time.
  • body.expressions[].column_name
    string · obrigatório · valores: regex ^[A-Z][A-Z0-9_]{0,44}$
    Name of this calculated column as it appears to the BI layer. Same alphabet as a field alias (A-Z, 0-9, , starts with a letter, max 45 chars), same H/WB_ID reservations. Must not collide with any fields[].alias or with another expression's column_name - the schema cannot check either, the server does, at push time.
  • body.expressions[].label
    string · obrigatório · valores: não vazio
    Human-readable caption for this calculated column wherever the BI layer lists/labels columns. Purely cosmetic.
  • body.expressions[].data_type
    string · obrigatório · valores: Number, Date, String
    Data type of the expression's result, as the BI layer will treat it. Must match what expression actually produces - the server does not infer or coerce this from the SQL.
  • body.expressions[].expression
    string · obrigatório · valores: não vazio
    SQL expression evaluated over this writeback's stored fields, e.g. "SUM(META_VALOR)". References field aliases as column names. Not validated for SQL correctness by this schema - a broken expression surfaces as a query error wherever the BI layer uses it, not at push time.
  • body.expressions[].mask
    string ou null · opcional
    Display mask for this calculated column, same convention as table column masks. null/omitted = no mask.
  • body.expressions[].behavior
    string ou null · opcional · valores: default, yearmonth, year, agp, avg, datetime
    How the BI layer aggregates/interprets this expression across time-based views (e.g. "yearmonth" for a value that should roll up by calendar month, "agp" for an already-aggregated/pre-computed value that should not be re-summed). null/omitted defaults to "default" (a plain additive measure) when read back from the server.
  • body.metadata_bi
    object · opcional
    Controls whether/which row-metadata (H_WB_*) columns this Cadastro exposes to the BI layer. Independent of the data-entry form itself.
  • body.metadata_bi.enabled
    boolean · opcional
    Exposes row-metadata columns (who created/edited each row and when, H_WB_* - created_at, author, updated_at, editor, version) to dashboards/apps built on this writeback. false = none of them are queryable from BI, regardless of hidden_columns.
  • body.metadata_bi.hidden_columns
    array de string · opcional
    H_WB_* metadata column names to keep hidden from BI even when enabled is true - an individual opt-out, e.g. hide H_WB_EDITOR while still exposing H_WB_CREATED_AT. Has no effect while enabled is false.

Exemplo ​

yaml
# writebacks/metas-por-filial--412.yaml
# yaml-language-server: $schema=https://docs.horusbi.com.br/schemas/v2/writeback.schema.json
id: 412
kind: writeback
lumo: v2
tenantId: 1200
---
name: Metas por Filial
alias: METAS_FILIAL_412          # nome físico, gerado pelo servidor; não mude
sections:
  - Identificação
  - Meta
fields:
  - alias: FILIAL
    label: Filial
    type: external_key           # valores vêm de uma tabela do HorusDW
    required: true
    section: Identificação
    ref:
      table: dim_filiais
      tableId: 6105              # o servidor liga por este id
      key: ID_FILIAL
      key_type: Number
      label_expr: NOME_FILIAL    # coluna mostrada no seletor
  - alias: PERIODO
    label: Período
    type: date
    granularity: month           # obrigatório em type: date
    required: true
    section: Identificação
  - alias: META_VALOR
    label: Meta (R$)
    type: number
    mask: MOEDA
    required: true
    section: Meta
  - alias: STATUS
    label: Status
    type: picklist               # vocabulário fechado, escrito aqui
    required: true
    section: Meta
    options:
      - value: Pendente
      - value: Aprovado
unique_constraints:
  - [FILIAL, PERIODO]            # uma meta por filial e mês

Tipos de campo ​

typePara quêExige
stringTexto livre
numberNúmero; a máscara vai em mask
dateDatagranularity: year, month, date ou datetime
external_keyEscolher uma linha de outra tabela do HorusDW (uma filial, um cliente)ref com tableId, key e label_expr
picklistEscolher num vocabulário fechado definido no próprio YAML (um status)options com pelo menos um item
imageEnviar uma imagem

Limites ​

  • fields[] é o esquema. Acrescentar um item e dar push cria a coluna física; tirar um item apaga a coluna e os dados dela. Por isso o push recusa quando um alias some de fields[] e pede que você diga o que foi: --rename ANTIGO=NOVO preserva os dados, --allow-drop ANTIGO apaga. Veja Drift e conflitos.
  • O alias do topo do corpo é o nome físico da tabela, que outros Cadastros referenciam pelo texto. O push que o muda é recusado sempre. Para mudar o nome que as pessoas veem, edite name.
  • Trocar o type de um campo que já tem dados pode perder os dados, conforme o tipo físico mude ou não. Confira com lumo writeback preview depois.
  • Não há key_type nem key_columns: a identidade da linha é a coluna reservada WB_ID, e nenhum item de expressions pode se chamar assim.
  • Um Cadastro publicado continua editável por push. Publicar de novo é recusado pelo servidor; não existe clone de Cadastro.
  • O --desk do publish é de Mesa de Dados (--desk 7 ou --desk dw-desk:7). Uma referência bi-desk:<id> é recusada antes de chegar ao servidor. Como os ids de Mesa de Dados e de Aplicação se repetem, confira o número com lumo list dw-desk.
  • lumo writeback import, imports, rollback-import e export servem para montar o projeto (carregar a planilha que o cliente mandou, desfazer uma carga errada), não para a digitação do dia a dia. Não rode import com a mesma planilha que já foi no --from-file: as linhas entram duas vezes.