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 JSON for readability, but in practice use .yaml files — 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

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/
}

Use depends to ensure referenced records exist before processing:

{
"data_type": "WindowAction",
"depends": ["product_views"],
"identifier": "products_action",
"name": "Products",
"default_view": "product_list_view"
}

Key points:

  • Use depends when a record references another record by identifier (e.g., default_view, search_view, action, parent, groups)
  • The value is a list of file names without extension (the loader resolves .yaml/.yml/.json)
  • Files are imported in dependency order before the current record is processed

Use relative paths to reference 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)

Common patterns:

FileTypical Dependencies
actions.jsonView files (["model_views", "other_views"])
menus.jsonAction files and security (["actions", "../security/security"])
View inheritance filesBase view files
Data files with groupsSecurity files (["../security/security"])

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": ["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.json:

[
{
"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
L (Link)["L", "identifier"]Add to existing relations
U (Unlink)["U", "identifier"]Remove from relations
R (Replace)["R", ["id1", "id2"]]Replace all relations
A (Unlink All)["A"]Remove all relations

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 R (Replace) 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 L (Link) when extending from another module (adds to existing):

{
"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 ["R", [...]] 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 ["R", [...]] (Replace) for M2M fields, not ["L", ...] (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 L command when extending records from other modules
  7. Use Replace for definitions - Use R command when defining the complete set in your own module