Example module acme/hello
The smallest complete module: one method greet({ name }) that stores greetings in the instance's collection, one method listGreetings for the table, one OVE page /hello with a datatable and a modal, and a Core.onEnabled handler. omm init scaffolds exactly these files; the source of the template is in the omm package, the docs keep a copy under docs/agents/examples/hello-module/.
It was checked with node --check index.js, omm pack --dry-run, a run of index.js against SDK 4.3.0 with a stubbed core, and a render of pages/hello.yaml in the OVE engine.
omm init writes the code of your module card in place of acme/hello. When you copy the files by hand, replace acme/hello in module.json, index.js and pages/hello.yaml yourself. Then follow the build loop.
module.json
{
"name": "acme/hello",
"version": "0.1.0"
}
package.json
{
"name": "acme-hello",
"version": "0.1.0",
"private": true,
"description": "Example Orbito module: stores greetings and lists them on an OVE page",
"main": "index.js",
"dependencies": {
"@orbito/api-lib": "^4.3.0",
"fastest-validator": "^1.19.1"
}
}
index.js
'use strict';
const path = require('path');
const Validator = require('fastest-validator');
const { Core, DB, Log, Pages } = require('@orbito/api-lib');
// The collection belongs to this installation only: the core namespaces it per instance.
const Greetings = new DB.Schema('greetings', {
name: { type: 'string', min: 1, max: 100 },
message: { type: 'string' },
createdAt: { type: 'date', optional: true, default: () => new Date() },
});
// One schema, two jobs: `params` documents the method for the core and the AI assistant,
// the compiled checker validates the input. The core never validates params by itself.
const greetParams = {
name: { type: 'string', trim: true, min: 1, max: 100 },
};
const checkGreetParams = new Validator().compile(greetParams);
Core.registerMethod('greet', {
description: 'Stores a greeting for the given name and returns it.',
params: greetParams,
returns: { type: 'object', props: { _id: 'string', message: 'string' } },
handler: async (params) => {
const input = { ...params };
const check = checkGreetParams(input);
if (check !== true) {
throw new Error(`INVALID_PARAMS: ${check.map((error) => error.message).join('; ')}`);
}
const message = `Hello, ${input.name}!`;
const greeting = await Greetings.create({ name: input.name, message });
Log.info('Greeting stored', { greetingId: String(greeting._id) });
return { _id: String(greeting._id), message };
},
});
Core.registerMethod('listGreetings', {
description: 'Lists stored greetings, newest first, one page at a time.',
params: {
page: { type: 'number', integer: true, positive: true, optional: true },
perPage: { type: 'number', integer: true, positive: true, max: 100, optional: true },
},
returns: { type: 'object', props: { data: 'array', total: 'number' } },
handler: async (params) => {
const query = params || {};
const page = Math.max(1, parseInt(query.page, 10) || 1);
const perPage = Math.min(100, Math.max(1, parseInt(query.perPage, 10) || 10));
const [data, total] = await Promise.all([
Greetings.find({}, perPage, (page - 1) * perPage, { createdAt: -1 }),
Greetings.count({}),
]);
// `{ data, total }` is the shape an OVE datatable reads without extra mapping.
return { data, total };
},
});
Pages.registerPages({
'/hello': {
name: 'Hello',
view: path.join(__dirname, 'pages', 'hello.yaml'),
placement: { location: 'navbar', icon: 'mood-smile', order: 100 },
},
}).catch((error) => {
Log.error('Page registration failed', { error: error.message });
});
// Runs once per installed version (first install and every update), not on every
// container start - use it for one-off setup such as indexes, never to start timers.
Core.onEnabled(async () => {
await DB.createIndex(Greetings.name, { createdAt: -1 });
Log.info('acme/hello enabled');
});
pages/hello.yaml
type: flex
direction: column
gap: 3
childs:
- type: datatable
id: greetingsTable
title: Greetings
perPage: 10
rowKey: _id
emptyMessage: No greetings yet
dataSource:
module: acme/hello
method: listGreetings
actions:
- label: Say hello
icon: plus
variant: primary
action: openModal
target: greetModal
columns:
- header: Name
field: name
- header: Message
field: message
- header: Created
width: 180px
content:
- type: text
content: '{{ formatDate greetingsTable.$.createdAt "YYYY-MM-DD HH:mm" }}'
muted: true
- type: modal
id: greetModal
title: Say hello
content:
type: input
id: greetNameInput
name: name
label: Name
placeholder: Ada
footer:
type: flex
direction: row
justify: end
gap: 2
childs:
- type: button
label: Cancel
variant: secondary
outline: true
onClick:
action: closeModal
target: greetModal
- type: button
label: Greet
variant: primary
onClick:
action: callMethod
module: acme/hello
method: greet
withData:
- { from: greetNameInput, as: name }
refresh: [greetingsTable]
onSuccess:
- action: closeModal
target: greetModal
- action: toast
type: success
content: '{{ $response.0.message }}'
onError:
- action: toast
type: alert
title: Could not greet
content: '{{ $error.message }}'
What happens at runtime
omm installcreates an instance in the company; the core starts its container withnode index.js.index.jsregistersgreet,listGreetingsand the/hellopage. The page gets a link in the top navigation bar (placement.location: navbar).- The core sends
onEnabledonce for this version: the module creates its index and logsacme/hello enabled(omm logs --json). - The page's datatable calls
listGreetingswith{ page, perPage }and showsdata. - Say hello opens the modal. Greet calls
greetwith{ name }taken from the inputgreetNameInput, reloads the table, closes the modal and shows the returnedmessagefrom$response.0.
Try it without the panel:
omm call greet --params '{"name":"Ada"}' --json
# {"_id":"65f0c3a1e4b0a1b2c3d4e5f6","message":"Hello, Ada!"}
omm call listGreetings --json
# {"data":[{"_id":"65f0c3a1e4b0a1b2c3d4e5f6","name":"Ada","message":"Hello, Ada!","createdAt":"2026-10-13T09:00:00.000Z"}],"total":1}
omm call greet --params '{"name":""}' --json
# fails, "error" carries: INVALID_PARAMS: The 'name' field length must be greater than or equal to 1 characters long.
