Skip to content

Module structure & manifest

Everything you build on Victor ships as a module — a self-contained folder with a module.yaml manifest at its root. A module bundles your data models, screens, agents, tools, skills, and automations into one installable unit. The Module Manager lists installable modules and lets you install, upgrade, and uninstall them per instance.

If you have written an Odoo addon, this will feel familiar: a manifest declares what the module contains, and the framework wires it up. The two big differences to keep in mind are covered below — the split between code keys and resource keys, and the role of __init__.py.

The manifest: module.yaml

Every module has a module.yaml at its root. Only two keys are required — name and version. Everything else is optional.

name: contact
label: Contacts
author: Victor Core
version: 0.1.0
requires: [base]
models: [models]
data: [views/contact.xml, data/contact.xml]

That is the real manifest for the built-in contact module. It declares its identity, depends on base, imports its models, and loads two XML files in order.

Required keys

Key Type Description
name string Unique technical id, snake_case and singular (e.g. crm, helpdesk). It is also the module's Python package name, so it must be a valid Python identifier. requires entries and install commands reference this value.
version string Version string, e.g. "0.1.0". Quote it so YAML doesn't read 0.1 as a float. The installed version is remembered per instance. Re-applying a module's definition happens when you run the Upgrade action (see Installing and upgrading) — it is not triggered automatically by a version change. Bumping version records the new installed version and is what brings a new batch of per-version migration scripts into range.

Metadata keys

Key Type Default Description
label string name Human-readable name shown in the Module Manager.
author string empty Who publishes the module. Informational only.

There is no category, icon, hidden, or description key

The manifest is deliberately small. To hide a module from the Module Manager, use installable: false (see Visibility & lifecycle) — there is no hidden key.

Dependencies

Key Type Default Description
requires list of module names [] Modules this one depends on. They are installed first (transitively), and this module is uninstalled before them.

Almost every module lists base here. base provides the core models that your agents, tools, skills, and workflows are seeded into (system parameters, sequences, agents, tools, skills, workflows, and more). If your module defines any of those, depend on base:

requires: [base]

Depend on other modules too when you extend them — for example a CRM built on top of contacts:

requires: [base, contact]

Two kinds of entries: code paths vs. file paths

This is the one distinction to internalize. Manifest entries come in two flavors, and they are addressed differently:

  • Code keys (models, agents, tools, actions) take dotted Python paths relative to your module package — like an import. models means "import the models package (or models.py file)"; agents.support means "import the support submodule of the agents package". No .py, no slashes.
  • Resource keys (data, workflows, automations, triggers) take file paths relative to the module folder — like views/contact.xml. These point at files on disk, with extensions and slashes.
  • skills takes directory names — folders, each holding one SKILL.md per sub-folder.
Manifest key Addressing Example value
models dotted Python path [models] or [models.crm_lead]
agents dotted Python path [agents] or [agents.sales, agents.support]
tools dotted Python path [tools.enrich]
actions dotted Python path [actions]
data file path [views/crm.xml, data/crm_seed.xml]
workflows file path [workflows/pipeline.yaml]
automations file path [automations/dunning.yaml]
triggers file path [triggers/daily.yaml]
skills directory name [skills]

Get the addressing right

Writing models: [models.py] (a file path) or data: [views.crm] (a dotted path) will not work. Code keys never take .py or slashes; resource keys always take a real path with its extension.

Code keys

Key Type Description
models dotted paths ORM model definitions. Registers your models so their tables are created. [models] imports a models.py file or a models/ package. See Models & fields.
agents dotted paths Agent definitions, seeded into the editable agent records. [agents] imports the agents/ package; [agents.sales, agents.support] imports named submodules. See Agents.
tools dotted paths Agent tools. [tools.enrich] imports tools/enrich.py. Seeded into the editable tool records. See Tools.
actions dotted paths Action handlers (server / record actions). See Actions & menus.

Resource keys

Key Type Description
data file paths Declarative XML: view + menu definitions and seed records. Loaded in listed order.
workflows file paths Workflow YAML files.
automations file paths Automation YAML files.
triggers file paths Trigger YAML files.
skills directory names Skill source folders (see below).

data loads top-to-bottom — order matters

List view/menu XML before the seed XML that references it. The contact module does exactly this: data: [views/contact.xml, data/contact.xml] — the views define the screens, then the seed data populates records the views display.

Standard folder layout

A typical module looks like this. Folders are conventions: a folder only does anything if a manifest key points at it. Single-file forms (models.py, views.xml) are equally valid.

crm/
├── module.yaml            # the manifest (required)
├── __init__.py            # makes `crm` an importable package (required if it ships any Python)
├── models/                # ORM model definitions  ->  models: [models]
│   ├── __init__.py        #   re-exports submodules so the model classes register
│   └── crm_lead.py
├── views/                 # view + menu XML       ->  data: [views/crm.xml]
│   └── crm.xml
├── data/                  # seed-record XML        ->  data: [data/crm_seed.xml]
│   └── crm_seed.xml
├── agents/                # agent definitions      ->  agents: [agents] or [agents.sales]
│   ├── __init__.py
│   └── sales.py
├── tools/                 # agent tools            ->  tools: [tools.enrich]
│   ├── __init__.py
│   └── enrich.py
├── skills/                # skills                 ->  skills: [skills]
│   └── greeter/
│       └── SKILL.md
├── workflows/             # workflow YAML          ->  workflows: [workflows/pipeline.yaml]
│   └── pipeline.yaml
├── automations/           # automation YAML        ->  automations: [automations/dunning.yaml]
│   └── dunning.yaml
├── triggers/              # trigger YAML           ->  triggers: [triggers/daily.yaml]
│   └── daily.yaml
└── migrations/            # per-version data migrations  ->  run on Upgrade (see below)
    └── 0.2.0/
        └── post-backfill.py

The role of __init__.py

Because code keys are Python imports, the module root and any Python sub-package (models/, agents/, tools/) must be importable Python packages — each needs an __init__.py.

A models/__init__.py should import its submodules, so the model classes are registered when the manifest imports the package:

# crm/models/__init__.py
from . import crm_lead, crm_stage  # noqa: F401  (imports register the model classes)

A module file under models/ is written against the public victor SDK — this is the code the models: key imports:

# crm/models/crm_lead.py
from victor import fields, models


class CrmLead(models.Model):
    name = "crm_lead"                      # technical model id (snake_case)
    label = "Lead"                          # human label
    _description = "A potential deal in the pipeline."
    display_name = fields.String(label="Name", required=True)
    email = fields.String(label="Email")
    stage = fields.Selection(
        options=[("new", "New"), ("won", "Won"), ("lost", "Lost")],
        label="Stage", default="new",
    )

No Python? No __init__.py

A module that ships no Python at all — a data-only manifest, or one that just hides itself — does not need an __init__.py. Only folders whose contents are imported (the root and any imported sub-package) must be packages.

See Models & fields for the full field reference.

Skills

A skill is a SKILL.md file with name + description frontmatter, living inside a sub-folder of a skills/ directory. The sub-folder name must match the frontmatter name. Point the skills key at the containing directory:

skills: [skills]
---
name: greeter
description: Greet the user warmly. Use when the user says hello.
---
# Greeter
Say hello back, warmly.

Each sub-folder under skills/ is one skill. Skills are seeded into the editable skill records. See Skills.

Visibility & lifecycle

Key Type Default Description
installable bool true Whether the module appears in the Module Manager as user-installable. Set false to hide it entirely.
default_install bool false true means the module is always installed on boot and can never be uninstalled. Reserved for core modules.
hooks mapping none Optional lifecycle hooks (see below).

For customer modules you normally leave both installable and default_install at their defaults. Setting installable: false is the way to keep a helper or internal module out of the Module Manager entirely.

Lifecycle hooks

hooks maps a phase to a Python function that runs around install. Two phases are recognized:

Phase When it runs
pre_init Before the module's schema and data are created.
post_init After the schema and data are loaded.

The value is "module:function" relative to your package. A bare "function" defaults the module part to hooks, i.e. a hooks.py. The function receives the current install context and may be sync or async.

hooks:
  pre_init: hooks:before_install    # runs before schema/data
  post_init: hooks:after_install    # runs after schema + data are loaded

Full annotated manifest

Every supported key, with only name and version required:

# ---- Required ----
name: crm                     # unique technical id (snake_case) = the module's Python package name
version: 0.1.0                # remembered per instance; bump it, then run Upgrade (brings new migrations/ into range)

# ---- Metadata (optional) ----
label: CRM                    # name shown in the Module Manager (defaults to `name`)
author: Acme Corp             # who publishes the module

# ---- Dependencies (optional) ----
requires: [base, contact]     # installed first (transitively); base provides the core models

# ---- Python code: DOTTED module paths, relative to this module's package ----
models: [models]              # imports models.py OR the models/ package (registers your models)
agents: [agents]              # or name submodules explicitly: [agents.sales, agents.support]
tools: [tools.enrich]         # imports tools/enrich.py
actions: [actions]            # action handlers

# ---- Declarative resources: FILE paths, relative to this module's folder ----
data:                         # loaded top-to-bottom: views/menus BEFORE the seed data that uses them
  - views/crm.xml
  - data/crm_seed.xml
workflows: [workflows/pipeline.yaml]
automations: [automations/dunning.yaml]
triggers: [triggers/daily.yaml]

# ---- Skills: DIRECTORY holding one sub-folder per skill (each with a SKILL.md) ----
skills: [skills]

# ---- Visibility / lifecycle (optional) ----
installable: true             # false hides the module from the Module Manager entirely
default_install: false        # true = always installed, can never be uninstalled (core modules)
hooks:                        # optional Python hooks around install
  pre_init: hooks:before_install   # "module:function" relative to the package; runs before schema/data
  post_init: hooks:after_install   # runs after schema + data are loaded

Minimal manifests

You rarely need every key. The smallest useful module is a manifest, a models.py, and a view file:

# smallest useful module
name: widget
label: Widgets
version: 0.1.0
models: [models]        # a models.py file next to module.yaml
data: [views.xml]

And to keep a module out of the Module Manager, installable: false is the whole manifest:

# hidden from the Module Manager
name: hidden
label: Hidden
version: 0.1.0
installable: false      # that's the whole manifest

Installing and upgrading

Manage modules from the Module Manager — the top-level Modules section inside your instance. Each module shows its state (installed / uninstalled) and version, with state-aware actions:

  • Install appears while the module is uninstalled. It sets up the module (installing everything in requires first) on this instance.
  • Upgrade appears whenever the module is installed. It re-applies your module's current definition without dropping data — new tables, columns, views, menus, and seed records are added, and seed records the module no longer ships are removed. The Upgrade action is always available on an installed module; it is not gated on a version change.
  • Uninstall removes the module and its data.

So the loop for shipping a change is: edit your module, deploy it, then click Upgrade (or POST /api/modules/{name}/upgrade). There is no "re-install" once a module is installed — the Install action disappears and Victor does not switch install to upgrade on its own; you use the distinct Upgrade action. The installed version is tracked per instance, so each instance upgrades independently. See Publishing & installing for the full deploy-then-install flow.

Per-version migration scripts

Bumping version matters when you need to transform existing data across releases. A module may ship migration scripts under migrations/<version>/, named by the stage they run in:

  • pre-*.py — before the schema is synced (adjust data before columns change)
  • post-*.py — after the schema and seed data are refreshed
  • end-*.py — after post

Each script defines a migrate function that receives a database session (use it to read and update data) and the version being upgraded from. It may be async or plain:

async def migrate(session, installed_version):
    # e.g. backfill a new column, or reshape values that changed meaning
    ...

During an Upgrade, a script under migrations/<version>/ runs when the instance's installed version is below that folder's version and at or below the version you are upgrading to — so bumping version is what brings a new migrations/<version>/ batch into range. Scripts under a special 0.0.0 folder run on every upgrade. This is how you carry data forward when a field's shape or meaning changes between releases.

Next steps