Authoring Agents

An Agentum agent is a plugin: a folder of JavaScript that runs inside Agentum’s background agent process. It has a manifest.json declaring identity and capabilities, an index.js exporting lifecycle hooks, and optional lib/ modules and a control-ui/panel.js dashboard. Agents run recurring work via ctx.schedule.every(ms, fn) and ctx.schedule.dailyAt('HH:MM', fn), or react to events. They reach the outside world through ctx.services.* (Telegram, AI, calendar, email, host-agent) and read other plugins’ data through ctx.data.read(pluginId, table).

You don’t need to know any of that to build one — the guided Create Agent flow handles it. This page is for when you want to go under the hood: hand-editing the source, building in your own editor, or understanding what the builder wrote.

Three ways to build

  1. The Create Agent flow — describe it in chat, Agentum drafts it. Covered in Creating Agents. Best first stop for everyone.
  2. Improve + a coding assistant — let Claude Code or Codex edit an existing agent with Agentum preparing the context and protecting your versions. Covered in Improving Agents.
  3. Fully hand-coded — your editor, your rules, against the format documented on this page.

Most people use (1) for the scaffold and (2) to refine; (3) is always available underneath.

The authoring kit: build in your own editor

If you work in Claude Code, Cursor, or any LLM-driven editor outside Agentum, export the agent authoring kit first — it’s the complete platform manual, written to be handed to an AI:

  • System → Advanced → Agent authoring kit → Export writes AGENTS.md (the prose contract), ctx.d.ts (editor autocomplete types), manifest.schema.json, and a working example-plugin/ to a folder you choose. There’s also a one-click Copy AGENTS.md for pasting straight into a chat.
  • Each of your agents also has Share Agent (in its ⋯ menu) — a single zip of the agent’s source plus the kit, ready to send to a collaborator or open in another tool.

The Improve flow injects this same kit automatically before every hand-off, which is why the coding assistants seem to “already know” the platform.

Hand-coding a plugin

Where plugins live

Each plugin is a folder under your agents directory (~/Documents/Agentum/agents by default — change it from System → Services → Agents folder). The folder name doesn’t have to match the plugin id, but by convention it does. Marketplace-installed agents live elsewhere, in an app-managed read-only area — you hand-edit only your own agents.

~/Documents/Agentum/agents/my-feature/
├── manifest.json        # identity, integrations declared, config schema
├── index.js             # init / routes / handlers
├── lib/                 # optional — shared modules
│   └── my-feature.js
├── control-ui/          # optional — custom dashboard panel
│   └── panel.js
├── data/                # auto-managed — backing files for ctx.store + ctx.data reads
├── store/               # optional — typed store schema (store/schema.js)
└── settings.json        # auto-generated, gitignored, mode 0600 — your config values

settings.json is per-user — it holds the values the user edits in your agent’s settings UI and is created on first save. Don’t commit it; don’t ship it.

Minimal manifest

{
  "id": "hello",
  "name": "Hello World",
  "version": "1.0.0",
  "description": "Pings me on Telegram every five minutes.",
  "enabled": true,
  "apiPrefix": "/api/hello",
  "permissions": ["telegram:send"],
  "uses": ["telegram"],
  "dataReads": []
}

That’s the floor. Important fields:

  • id — must be unique across your installed plugins. Kebab-case.
  • uses — integrations your plugin calls (telegram, ai, calendar, email, hostAgent). Auto-granted on install; the user can revoke specific entries from the agent’s Integrations panel.
  • dataReads — cross-plugin reads (<sourcePluginId>/<table>). Empty array if you don’t need any. Auto-granted on install.
  • permissions — capability hints used by the integration-inference fallback. Optional; informational.

For user-tunable settings (an API key, an interval, a mode toggle), add a config block in the manifest. Each field declares a type (string, credential, number, boolean, select) with optional defaults. The declared defaults merge into ctx.pluginConfig; user edits (from the agent’s settings UI) persist to settings.json:

"config": {
  "interval": { "type": "number", "default": 300, "label": "Interval (s)" },
  "mode":     { "type": "select", "default": "auto", "label": "Mode",
                "options": [{"value":"auto","label":"Auto"},
                            {"value":"manual","label":"Manual"}] }
}

You don’t need tickIntervalMs, tickId, or tickRunImmediately in the manifest anymore — register recurring jobs inside init(ctx) (see below). The old manifest tick fields still work as a back-compat shim for older plugins, but new code shouldn’t use them.

Minimal index.js

'use strict';

const { log } = require('../../core/utils/logger');

module.exports = {
  id: 'hello',

  init(ctx) {
    log('[hello] init');

    // Recurring work. Pick ONE of these patterns:

    // Every five minutes
    ctx.schedule.every(300_000, async () => {
      const chatId = ctx.config.TELEGRAM_ALLOWED_USER_ID;
      if (!ctx.services.telegram.isReady()) return;
      await ctx.services.telegram.send(chatId, 'Hello from your agent.');
    }, { tickId: 'hello' });

    // Or once a day at 07:30 local
    // ctx.schedule.dailyAt('07:30', async () => { … }, {
    //   timezone: 'America/Los_Angeles',
    //   tickId: 'hello-morning',
    // });
  },
};

Just one required hook — init(ctx) runs once when the agent process starts. Register your slash commands, your scheduled jobs, your event handlers, your ctx.fn exports — all in one place.

The scheduled function can throw freely; the scheduler catches errors and logs them with plugin attribution. Pass tickId: 'your-plugin-id' if you want a friendly name in the Schedulers tab.

The ctx object

Field What it is
ctx.config Read-only host configuration values (managed by the app’s Settings UI). Use for shared infrastructure credentials (Telegram bot token, LLM API keys, SMTP).
ctx.pluginConfig Your plugin’s own settings, merged from settings.json over manifest.config defaults. Use for plugin-private values.
ctx.schedule.every(ms, fn, opts?) Register a recurring job. Returns { tickId, unregister() }.
ctx.schedule.dailyAt('HH:MM', fn, opts?) Register a daily-fire job in opts.timezone (IANA name).
ctx.services.telegram send, sendAudio, edit, callApi, isReady — the Telegram bot surface.
ctx.services.ai (or gemini) generate(prompt), runSession, transcribeAudio, isReady. Dispatches across providers based on user’s LLM choice.
ctx.services.calendar fetchEvents, fetchBusyTimes, fetchRecent, findFreeSlots, isReady.
ctx.services.email send, health, testRoundTrip, isReady.
ctx.services.hostAgent Reach the macOS host: keepSave (Reminders), powerSleep, sessionsList, sessionsOpen.
ctx.data.read(pluginId, table) Read another plugin’s data/<table>.json. Returns a deep-cloned array (mutating it doesn’t affect the source). Returns [] on any failure — denied, missing, malformed.
ctx.store Typed per-plugin store (when you ship store/schema.js). get, set, query, mutate, delete. Persists to data/<table>.json.
ctx.plugins.isEnabled(id) Is plugin X currently enabled? Use for graceful degradation when a sibling might not be installed.
ctx.events Pub/sub bus. ctx.events.on('x', handler) / ctx.events.emit('x', data).
ctx.fn Cross-plugin function registry. Other plugins register helpers here for you to call.

Always guard service calls with isReady() if your plugin should degrade gracefully when an integration isn’t configured or the user has revoked the grant:

if (ctx.services.calendar.isReady()) {
  const events = await ctx.services.calendar.fetchEvents();
  // ...
}

Calling an un-granted integration’s methods directly (without the isReady() guard) returns a rejected promise with a clear message: “Integration X is not granted to this plugin. Grant it from the Permissions panel in the main app.” Helpful, but you should still guard for the user who hasn’t configured the integration at all.

Permissions and grants

Two declarations in your manifest control what your plugin can touch:

  • uses — integrations (telegram, ai, calendar, email, hostAgent)
  • dataReads — cross-plugin reads (<sourceId>/<table> strings)

When the user installs your plugin, Agentum auto-grants both lists. The user can revoke individual entries from the agent’s Integrations panel (in its ⋯ menu) — flipping a toggle off makes the corresponding access return a denial stub (for integrations) or an empty array (for data reads), and the plugin reloads so your code sees the new state immediately.

This means your plugin should treat “not granted” the same way it treats “not configured” — guard ctx.services.X calls with isReady(), and check the length of ctx.data.read(...) results before using them. A well-behaved plugin keeps running with reduced capability rather than crashing on revoked access.

Cross-plugin data reads

The dataReads declaration lets your plugin compose information from other plugins. The classic case is a morning digest pulling forecasts, headlines, and market signals from weather/news/polymarket and sending one message:

// manifest.json
{
  "id": "morning-digest",
  "uses": ["telegram", "ai"],
  "dataReads": ["weather/forecast", "news/headlines", "polymarket/markets"]
}
// index.js
init(ctx) {
  ctx.schedule.dailyAt('07:30', async () => {
    const forecast  = ctx.data.read('weather', 'forecast');
    const headlines = ctx.data.read('news', 'headlines');
    const markets   = ctx.data.read('polymarket', 'markets');

    if (!forecast.length && !headlines.length && !markets.length) return;

    const summary = await ctx.services.ai.generate(
      `Compose a 4-sentence morning digest from: ${JSON.stringify({ forecast, headlines, markets })}`,
    );
    await ctx.services.telegram.send(
      ctx.config.TELEGRAM_ALLOWED_USER_ID,
      summary,
      { parse_mode: 'Markdown' },
    );
  }, { timezone: 'America/Los_Angeles', tickId: 'morning-digest' });
}

A few things to know:

  • Read-only by design. Cross-plugin writes are not supported. If you need another plugin to do something, emit an event with ctx.events.emit('your-plugin:something-happened', payload) and let that plugin subscribe.
  • Empty array on every failure mode. Denied grant, missing target plugin, missing table file, malformed JSON — they all return []. So if (forecast.length) is the cleanest way to handle the “not available” case.
  • Loader ordering is automatic. A plugin declaring dataReads: ["weather/forecast"] is initialised after weather — no explicit loadAfter needed.

HTTP routes (optional)

If you’re building a custom dashboard panel, you’ll want to expose data to it. Add a routes(router, ctx) export:

routes(router, ctx) {
  router.get('/api/hello/status', async ({ res }) => {
    res.writeHead(200, { 'Content-Type': 'application/json' });
    res.end(JSON.stringify({ status: 'ok', lastTick: Date.now() }));
  });
},

Your panel then calls ctx.api('/api/hello/status') to read it. The full panel.js contract — what’s on ctx, lifecycle hooks, hot-reload semantics — lives in the authoring kit’s AGENTS.md and example-plugin/ (export it from System → Advanced → Agent authoring kit); copying the example is the fastest way to get started.

Hot reload

Saving a control-ui/panel.js file hot-reloads the panel in the running app — under a second, no restart. Changes to index.js, manifest.json, or anything in lib/ need an agent restart: SystemServicesRestart. One click, a few seconds — it restarts the background agent process, not the app.

Changes to grants (Integrations toggles) DO hot-reload — the plugin’s init() re-runs and your captured grant set refreshes. No restart needed.

Sharing an agent

For one-off sharing — sending a plugin to a teammate or a friend — a plugin is just a folder, so:

  1. Zip the plugin folder.
  2. Send it.
  3. The recipient drops it into their plugins folder. The file watcher picks up the new folder and offers to load it — confirm to install and grant the integrations the plugin declares.

For wider distribution to any Agentum user, publish to the marketplace — see Publishing to the Marketplace. The publish flow is a wizard in the desktop app; the result is a card on the public marketplace that anyone with an active license can install with one click. Versioning, channels, updates, and screenshots are first-class.

settings.json is per-user and gitignored by convention. Whichever way you share, ship manifest.json and index.js (and lib/ and control-ui/ if present); let each user fill in their own keys in the agent’s settings and grant integrations from its Integrations panel.

Next steps