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:
Depend on other modules too when you extend them — for example a CRM built on top of contacts:
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 animport.modelsmeans "import themodelspackage (ormodels.pyfile)";agents.supportmeans "import thesupportsubmodule of theagentspackage". No.py, no slashes. - Resource keys (
data,workflows,automations,triggers) take file paths relative to the module folder — likeviews/contact.xml. These point at files on disk, with extensions and slashes. skillstakes directory names — folders, each holding oneSKILL.mdper 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:
---
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
requiresfirst) 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 refreshedend-*.py— afterpost
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¶
- Models & fields — define the data your module stores.
- Views and Menus — the screens loaded via
data. - Agents, Tools, and Skills — the AI surface seeded from your module.
- Workflows, Automations, and Triggers — declarative behavior loaded via YAML.