Orbito - Dokumentacja modułówOrbito - Dokumentacja modułów
Wprowadzenie
Pierwsze kroki
API
Komunikacja
Widoki
Wdrożenie
Społeczność
For AI agents
Wprowadzenie
Pierwsze kroki
API
Komunikacja
Widoki
Wdrożenie
Społeczność
For AI agents
  • For AI agents

    • Orbito for AI agents
    • Module anatomy
    • The build loop
    • Cheatsheet
    • Example module acme/hello
    • Company automations
    • Changelog

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

  1. omm install creates an instance in the company; the core starts its container with node index.js.
  2. index.js registers greet, listGreetings and the /hello page. The page gets a link in the top navigation bar (placement.location: navbar).
  3. The core sends onEnabled once for this version: the module creates its index and logs acme/hello enabled (omm logs --json).
  4. The page's datatable calls listGreetings with { page, perPage } and shows data.
  5. Say hello opens the modal. Greet calls greet with { name } taken from the input greetNameInput, reloads the table, closes the modal and shows the returned message from $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.
Ostatnio zaktualizowano:: 10/13/26, 9:44 AM
Contributors: Wiktor Koźmiński, Claude Fable 5.1
Prev
Cheatsheet
Next
Company automations