Skip to content

Data Files

Data files load initial records when a module is installed.

File format: YAML is the convention. Every shipped module writes its data files as .yaml (the loader also accepts .json/.yml). The examples below are shown in YAML; the record shape (data_type, identifier, fields, command arrays) is identical in both formats.

Place data files (.yaml) in the appropriate directories — no manifest data: key is needed; the loader auto-discovers every .yaml/.yml/.json file (except those starting with _) under views/, data/, security/, and templates/ (plus demo/ in demo mode):

modules/your_module/
├── views/
│ ├── views.yaml # UI views (UiView)
│ ├── actions.yaml # Window actions (WindowAction)
│ └── menus.yaml # Menus (UiMenu)
├── data/
│ ├── stages.yaml # Seed data (user-customizable)
│ └── categories.yaml # Reference data
├── security/
│ └── security.yaml # Groups and permissions
└── templates/
├── section_templates.yaml # Template definitions
├── hero.jinja # Jinja template files
└── report_layout.jinja # Report templates
FolderPurposeRecord Types
views/UI definitionsUiView, WindowAction, UiMenu
data/Seed/reference dataAny model (stages, currencies, categories)
security/Access controlGroup, ModelAccess, RecordRule
templates/All templatesWebsiteSectionTemplate, ReportLayout, WebPage

Files starting with _ are ignored (e.g., _draft.json).

Data files are arrays of record definitions (a YAML list, or a JSON array):

- data_type: ModelName
identifier: unique_identifier
field1: value1
field2: value2
FieldDescription
data_typeModel class name
identifierUnique identifier for updates

Controls whether records are updated on module update:

- data_type: CrmStage
identifier: crm_stage_new
name: New
sequence: 1
apply_once: true
ValueBehavior
trueOnly create if not exists (preserves user customizations)
false (default)Always create/overwrite on update

When to use apply_once: true:

  • Seed data users may customize (stages, categories, currencies, payment terms)
  • Reference data with user-editable fields

When to use apply_once: false (or omit):

  • System records that must stay in sync (cron jobs)
  • UI definitions (views, actions, menus) - these are managed by the module system
  • Security records (groups, model access) - should always match code

The loader applies partial updates: on re-import it only writes the fields your record actually lists, and leaves any column you didn’t mention untouched. So to clear a field on an already-stored record — e.g. detaching a menu from its parent to promote it to a top-level app — set it to null explicitly (parent: null); simply deleting the parent: line is a no-op on the existing row (though a fresh install with no parent: is created top-level).

Template files can be referenced from JSON by filename. The loader auto-detects file references based on known template extensions:

- data_type: WebsiteSectionTemplate
identifier: hero_centered
template: hero_centered.jinja

Supported extensions: .jinja, .jinja2, .html, .xml, .txt, .md, .css, .js

How it works:

  • String fields with supported template extensions
  • That don’t start with http://, https://, or /
  • Are treated as file references relative to the JSON file
  • The file content is loaded into the field

Examples:

template: hero.jinja # Loads ./hero.jinja
content: ../templates/block.jinja # Loads from parent/templates/
layout: layouts/standard.jinja # Loads from ./layouts/

Date and Datetime field values may be written as a relative-date token instead of a fixed calendar date. The loader resolves it at import time against the install date, so seed and demo data stays anchored to “now” and never goes stale — a database installed today and one installed a year from now both render a live, non-flat trend in date-grouped reports instead of every record landing on install-day.

- data_type: SaleOrder
identifier: demo_order_acme_001
date: "@today-90d" # 90 days before install
validity_date: "@today+7d" # 7 days after install

Grammar: an anchor (@today for a date, @now for a datetime) followed by zero or more signed offsets <+|->N<unit>:

UnitMeaningExample
ddays@today-45d
wweeks@today-2w
mmonths@today-3m
yyears@today-1y+15d
hhours (datetime only)@now-2h

@today yields a calendar date; @now yields a UTC timestamp. On a Datetime field, @today lands at midnight UTC so date-grouped reports bucket cleanly. A malformed token (e.g. @today-45) fails loudly at import rather than silently resolving to today. A value that doesn’t start with @ is treated as a literal date exactly as before.

Note: audit fields like created_date are stamped by the ORM on create and can’t be set from a data file — back-date those in a post_install hook if a report keys on them.

Within one module, file order is handled for you. Files are imported in a fixed, alphabetical order per directory, and a reference to an identifier that hasn’t been imported yet pulls in the file that declares it first — so menus.yaml referencing an action declared in actions.yaml resolves regardless of which file is read first. A reference that no file in the module (or its dependency modules) declares still fails at import, naming the file, field and identifier.

Use depends when you need to order imports for a reason the loader can’t infer from a reference — most often a file in another directory whose records must exist first:

- data_type: WindowAction
depends:
- product_views
identifier: products_action
name: Products
default_view: product_list_view

Key points:

  • The value is a list of file names without extension (the loader resolves .yaml/.yml/.json)
  • Files listed in depends are imported before the current record is processed
  • Records in other modules are available because those modules install first — list the module in the manifest’s dependencies, not in depends

Automatic resolution spans a module’s views/, data/, security/ and templates/ directories, so a menu in views/ referencing a group declared in security/ needs no depends. The one exception is demo/: it is imported only on a demo database, so a file outside demo/ never pulls a record out of it — a reference from views/ or data/ to a demo-only identifier fails, as it must.

depends remains available, and takes relative paths for files in other directories:

- data_type: UiMenu
depends:
- actions
- ../security/security
identifier: admin_menu
name: Admin Menu
action: admin_action
groups:
- - L
- my_module_admin
Path FormatDescription
"actions"Same directory (views/actions.yaml)
"../security/security"Parent + subdirectory (security/security.yaml)
"../data/seed_data"Parent + subdirectory (data/seed_data.yaml)

These are ordering hints, not requirements: a reference to an identifier declared anywhere in the same module resolves on its own. Reach for depends when a file must be imported first for a reason no reference expresses — for example seed records whose calculate or create override reads records another file creates.

All other fields map to model fields:

- data_type: Product
identifier: product_laptop
name: Laptop
price: 999.99
active: true
- data_type: Group
identifier: sales_user
name: User
category: Sales
- data_type: ModelAccess
identifier: product_access
name: Product Access
model: Product
group: sales_user
read_perm: true
write_perm: true
create_perm: true
delete_perm: false
- data_type: UiView
identifier: product_list_view
name: product_list_view
type: List
model: Product
arch: [...]
- data_type: WindowAction
identifier: products_action
name: Products
model: Product
modes: List,Form
views:
- - R
- - product_list_view
- product_form_view
- data_type: UiMenu
identifier: products_menu
name: Products
parent: sales_menu
action: products_action
sequence: 10
- data_type: CrmStage
identifier: stage_new
name: New
sequence: 10
fold: false

Reference by identifier:

- data_type: Product
identifier: product_phone
name: Smartphone
category: category_electronics

The system resolves category_electronics to the actual record ID.

- data_type: UiMenu
identifier: sub_menu
name: Sub Menu
parent: main_menu
  1. Security groups (Group)
  2. Model access rules (ModelAccess)
  3. Views (UiView)
  4. Actions (WindowAction)
  5. Menus (UiMenu)
  6. Other data (sorted by dependencies)

When a module is updated:

  1. Records with existing identifiers are updated
  2. New identifiers create new records
  3. Removed identifiers keep existing data (no automatic deletion)

modules/crm/data/stages.yaml:

- data_type: CrmStage
identifier: stage_new
name: New
sequence: 10
probability: 10
fold: false
- data_type: CrmStage
identifier: stage_qualified
name: Qualified
sequence: 20
probability: 30
fold: false
- data_type: CrmStage
identifier: stage_proposition
name: Proposition
sequence: 30
probability: 60
fold: false
- data_type: CrmStage
identifier: stage_won
name: Won
sequence: 100
probability: 100
fold: true
- data_type: CrmStage
identifier: stage_lost
name: Lost
sequence: 110
probability: 0
fold: true

When your module needs to modify records defined by another module (e.g., adding groups to an existing menu, adding implied_groups to a group), use the same identifier with command arrays for M2M fields.

  1. Same identifier = update existing record
  2. Only specified fields are modified
  3. Command arrays control how M2M/O2M fields are updated
CommandSyntaxDescription
link["link", "identifier"]Attach to the existing relations
unlink["unlink", "identifier"]Detach from the relations
set["set", ["id1", "id2"]]The relations become exactly these
clear["clear"]Detach every relation

Your module can add implied groups to an existing group:

- data_type: Group
identifier: sales_manager_group
implied_groups:
- - L
- my_new_group
- - L
- another_group

This adds groups to the existing implied_groups (doesn’t replace).

Change an existing menu’s parent or sequence:

- data_type: UiMenu
identifier: crm_pipeline_menu
parent: my_module_root_menu
sequence: 5

Add your custom view to an existing action:

- data_type: WindowAction
identifier: pipeline_action
views:
- - L
- my_custom_pipeline_view

Restrict an existing menu to specific groups:

- data_type: UiMenu
identifier: admin_menu
groups:
- - L
- my_admin_group

Use set when defining the complete set in the original module:

- data_type: Group
identifier: sales_admin
name: Sales Administrator
implied_groups:
- - R
- - sales_user_group
- crm_user_group

Use link when extending from another module (adds to what is already there):

- data_type: Group
identifier: sales_admin
implied_groups:
- - L
- my_extra_permission_group

Any record type with an identifier can be extended:

Record TypeCommon Extensions
GroupAdd implied_groups, change category
UiMenuChange parent, sequence, action, add groups
WindowActionAdd views, modify context
ModelAccessChange permissions
RecordRuleModify domain
Seed dataModify defaults

To scaffold a new module skeleton (directories, manifest, __init__.py), use:

Terminal window
fullfinity-server new my_module
# --path <dir> to choose where it is created (default: current directory)

Record extensions are then written by hand in the new module’s data files — add a record with the same identifier as the record you want to extend (see the command-array examples above).

When a module is uninstalled:

  1. Records owned by the module are deleted - only records where module field matches the uninstalling module
  2. Dependencies are updated - all parent modules are updated to restore their original state
  3. Extensions are automatically reverted - because parent modules re-import their JSON files with ["set", [...]] commands

This means:

  • Scalar field changes made by the uninstalled module are restored
  • M2M links added by the uninstalled module are removed
  • ManyToOne references to deleted records are restored to originals

Important: For this to work correctly, original modules should use ["set", [...]] for M2M fields, not ["link", ...].

  1. Use descriptive identifiers - product_laptop not prod1
  2. Group related data - Separate files for stages, tags, etc.
  3. Order matters - Dependencies must be defined first
  4. Immutable identifiers - Never change identifiers after release
  5. Keep data minimal - Only include essential seed data
  6. Use Link for extensions - Use link command when extending records from other modules
  7. Use Replace for definitions - Use set command when defining the complete set in your own module