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
- The Create Agent flow — describe it in chat, Agentum drafts it. Covered in Creating Agents. Best first stop for everyone.
- 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.
- 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 workingexample-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
[]. Soif (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 explicitloadAfterneeded.
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: System → Services → Restart. 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:
- Zip the plugin folder.
- Send it.
- 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
- Ready to share your agent? Publishing to the Marketplace
- New to Agentum? Getting Started walks the install.
- Want to navigate the dashboard first? Using Agentum.
- Stuck? Troubleshooting.