Add a Module¶
A module is deterministic backend logic — CRUD actions, domain data, business rules. It runs without AI. For AI-driven behavior, use a workflow instead.
Modules and workflows complement each other: modules provide the action surface that AI agents call.
When to Add a Module¶
Add a module when you need:
- a persistent entity (users, orders, projects, messages)
- an API endpoint that pages or workflows call
- business logic that should not require an LLM
- events that other modules or workflows react to
If the behavior requires planning, reasoning, or generation, use a workflow and have it call module actions for persistence.
File Structure¶
app/modules/{name}/
├── module.yaml ← required: identity, actions, permissions
├── contracts/ ← add only the contracts this module needs
│ ├── events.yaml ← domain events this module publishes
│ ├── reactions.yaml ← events from other modules this module handles
│ ├── notifications.yaml ← notification rules per event
│ ├── settings.yaml ← user-configurable settings schema
│ ├── admin.yaml ← optional feature panels for the framework admin shell
│ └── entitlements.yaml ← capability gates per plan or role
├── runtime_extensions.yaml ← only for raw webhook routers or background workers
└── backend/
├── __init__.py
├── handler.py ← required: thin dispatch, one method per action
├── service.py ← all business logic and event emission
├── repo.py ← MongoDB access only, no logic
├── policy.py ← query scoping for multi-tenancy
└── schemas.py ← typed document shapes and pure helpers
Only module.yaml and backend/handler.py are required. Add contracts and backend files only when the module needs them.
Use contracts/reactions.yaml as the canonical event-reaction contract. The runtime still accepts deprecated contracts/subscriptions.yaml only as a temporary fallback when contracts/reactions.yaml is absent, but new contributor-facing changes should author contracts/reactions.yaml only.
The runtime auto-discovers and registers all modules at startup. No registration step is needed. Routes are auto-mounted at /api/modules/{name}/{action_id}.
Layer Rules¶
| Layer | Do | Don't |
|---|---|---|
handler.py | Receive ctx + kwargs, call service, return result | Business logic, DB access, event emission |
service.py | Validate, call repo, emit events after commit | Direct DB access, HTTP calls |
repo.py | MongoDB queries only | Business logic, validation, events |
policy.py | Build query dicts from ctx | DB access, side effects |
schemas.py | TypedDicts, timestamp helpers | I/O, imports from service or repo |
Runtime Data Integrity¶
Modules own runtime facts. UI primitives should only render values returned by module actions, so module services must not manufacture fake product data.
For generated and hand-authored modules:
- do not return sample, demo, mock, fake, placeholder, or random records from runtime actions
- do not hardcode KPI counts, balances, totals, percentages, or status trends
- do not leave
TODO,NotImplemented, or "in production" branches in module runtime paths - compute
*_summary,*_stats,*_metrics, andget_*_countvalues fromrepo.py/ MongoDB queries - return honest empty values such as
0,[], ornullwhen no data exists - return trend/change fields only when the module queries a real historical comparison or metrics snapshot; otherwise return
nullor omit the field
This keeps pages and admin panels reusable: SummaryStrip and Metric render module output, while modules remain responsible for truth and provenance.
Minimum module.yaml¶
schema_version: mozaiks.module.v1
module:
id: {name}
display_name: {Display Name}
version: 1.0.0
description: What this module does.
owner: mozaiks
visibility: internal
handler: backend.handler:{Name}Handler
permissions:
- id: {name}.read
description: Read {name} data.
- id: {name}.manage
description: Create and update {name} records.
actions:
- id: list_{name}s
description: List records.
handler_method: list_{name}s
api_surface: public_readonly
input_schema:
type: object
properties:
limit: { type: integer }
output_schema:
type: object
required: [items, count]
permissions: [{name}.read]
- id: create_{name}
description: Create a record.
handler_method: create_{name}
input_schema:
type: object
required: [name]
properties:
name: { type: string }
output_schema:
type: object
required: [success]
permissions: [{name}.manage]
emits: [domain.{name}.record_created]
api_surface is optional action metadata. Use it to describe the intended surface for tooling and generated UI/API documentation, for example public, public_readonly, internal, or admin_internal. It is not an authorization mechanism; backend access remains governed by the action's permissions.
Events¶
Declare domain events in contracts/events.yaml when other modules or workflows need to react to this module's actions.
schema_version: mozaiks.events.v1
events:
- type: domain.{name}.record_created
version: 1
description: Emitted when a record is created.
producer: {name}
payload_schema:
type: object
required: [record_id, owner_id]
Add reactions in contracts/reactions.yaml when this module needs to handle events from other modules. The event payload fields are unpacked as keyword arguments into the handler method.
Connect to a Page¶
Point a page's DataTable directly at the module action endpoint:
# app/ui/pages/items.yaml
sections:
- id: items_table
primitive: DataTable
config:
api_endpoint: /api/modules/{name}/list_{name}s
columns:
- { key: name, label: Name }
Connect to a Workflow¶
Call module actions from workflow tools using the backend request helper:
from mozaiksai.core.workflow.app_backend_tools import backend_request
result = await backend_request(
method="POST",
path="/api/modules/{name}/create_{name}",
body={"name": "example"},
context_variables=context_variables,
)
Admin Panel¶
A module may include contracts/admin.yaml when it needs feature-owned panels inside the framework admin shell. AppGenerator can derive these panels from declared actions, or you can write the contract by hand.
Derivation rules (AppGenerator):
| Action pattern | Panel type | Default section |
|---|---|---|
list_*, search_* (collection) | ResourceTable / DataTable | overview |
*_stats, *_summary, *_metrics | SummaryStrip | same as record panel |
approve_*, reject_*, publish_* | row action on primary table | operations |
Section is inferred from the module's entity domain: - billing / subscription / payment → billing - user / account / member / profile → users - request / queue / approval / job → operations - usage / quota / limit / metric → usage - setting / preference / config → settings - everything else → overview
Example — a hosting_requests module with list_requests and get_summary actions produces:
schema_version: mozaiks.admin.v2
panels:
- id: hosting_requests.summary
label: Hosting Overview
section: operations
order: 1
renderer: schema
layout: full-width
sections:
- id: summary-strip
primitive: SummaryStrip
config:
api_endpoint: /api/modules/hosting_requests/get_summary
items:
- { label: Pending, value_key: pending_count }
- id: hosting_requests.requests
label: Hosting Requests
section: operations
order: 2
renderer: schema
layout: full-width
sections:
- id: requests-table
primitive: ResourceTable
config:
api_endpoint: /api/modules/hosting_requests/list_requests
columns:
- { key: app_name, label: App }
- { key: status, label: Status, type: status }
- { key: requested_at, label: Requested }
To add a panel manually when the generator didn't derive one:
schema_version: mozaiks.admin.v2
panels:
- id: {name}.overview
label: {Display Name}
section: overview
order: 10
renderer: schema
layout: full-width
sections:
- id: {name}-table
primitive: DataTable
config:
api_endpoint: /api/modules/{name}/list_{name}s
columns:
- { key: name, label: Name }
Valid section values: overview, users, billing, usage, activity, operations, settings, integrations, support.
The admin runtime auto-discovers contracts/admin.yaml at startup — no registration step needed. Panels appear inside the framework-owned admin surface under the declared section. They are not Console app portfolio pages.