Agent Recipes
Once you’ve read Authoring Agents, the fastest way to build is to start from a working pattern. These recipes are short, complete, and meant to be edited.
1. Daily digest
Fires once a day at a user-configurable time, composes a message with the AI, and delivers it into Agentum Chat. The canonical “do something every morning” plugin.
// manifest.json
{
"id": "morning-ping",
"name": "Morning Ping",
"version": "1.0.0",
"description": "A daily personalised greeting in your chat.",
"enabled": true,
"apiPrefix": "/api/morning-ping",
"permissions": ["chat:send", "ai:gemini"],
"uses": ["ai"],
"consumes": ["chat-providers/delivery"],
"config": {
"enabled": { "type": "boolean", "default": true, "label": "Enable morning ping" },
"fireAt": { "type": "string", "default": "07:30", "label": "Fire time (HH:MM, local)" },
"tone": { "type": "select", "default": "warm", "label": "Greeting tone",
"options": [
{ "value": "warm", "label": "Warm" },
{ "value": "dry", "label": "Dry-witted" },
{ "value": "stern","label": "Stern" }
]}
}
}
// index.js
'use strict';
const { log } = require('../../core/utils/logger');
module.exports = {
id: 'morning-ping',
init(ctx) {
log('[morning-ping] init');
ctx.schedule.dailyAt(ctx.pluginConfig.fireAt, async () => {
if (!ctx.pluginConfig.enabled) return;
if (!ctx.services.ai.isReady()) return;
const message = await ctx.services.ai.generate(
`Write a one-sentence ${ctx.pluginConfig.tone} good-morning greeting to me.`,
);
// Delivery is never guarded — the user is always reachable.
await ctx.plugins.api('agentum.chat-providers/delivery').send({
agentId: 'morning-ping',
channel: 'agentum-chat',
text: message,
});
}, {
timezone: 'America/Los_Angeles',
tickId: 'morning-ping',
});
},
};
Why it works: ctx.pluginConfig.fireAt is read at schedule registration, so the user’s config value picks up at install. The isReady() guard on the AI means the plugin keeps running gracefully when the user hasn’t configured an LLM, or has revoked it from the Integrations panel. Note what is not guarded: the send. Agentum Chat ships with the app, so there is always somewhere to put a message — guarding delivery is how an agent ends up silently never messaging anybody.
2. Cross-plugin morning digest
The motivating use case for ctx.data.read — read several other plugins’ tables, compose a digest, deliver it. Declare the reads in manifest.dataReads and the loader auto-grants them on install.
// manifest.json
{
"id": "morning-digest",
"name": "Morning Digest",
"version": "1.0.0",
"description": "Aggregates weather, news, and markets into a single morning briefing.",
"enabled": true,
"permissions": ["chat:send", "ai:gemini"],
"uses": ["ai"],
"consumes": ["chat-providers/delivery"],
"dataReads": [
"weather/forecast",
"news/headlines",
"polymarket/markets"
],
"config": {
"fireAt": { "type": "string", "default": "07:30", "label": "Fire time" }
}
}
// index.js
init(ctx) {
ctx.schedule.dailyAt(ctx.pluginConfig.fireAt, async () => {
const forecast = ctx.data.read('weather', 'forecast');
const headlines = ctx.data.read('news', 'headlines');
const markets = ctx.data.read('polymarket', 'markets');
// ctx.data.read returns [] on any failure — denied grant,
// missing target, missing file, malformed JSON. Treat empty
// arrays as "not available" and degrade.
if (!forecast.length && !headlines.length && !markets.length) {
return;
}
const summary = await ctx.services.ai.generate(`
Compose a 4-sentence morning digest. Be specific; cite numbers.
WEATHER: ${JSON.stringify(forecast.slice(0, 1))}
NEWS: ${JSON.stringify(headlines.slice(0, 5))}
MARKETS: ${JSON.stringify(markets.slice(0, 3))}
`);
await ctx.plugins.api('agentum.chat-providers/delivery').send({
agentId: 'morning-digest',
channel: 'agentum-chat',
text: summary,
});
}, { timezone: 'America/Los_Angeles', tickId: 'morning-digest' });
}
Loader ordering is automatic — declaring dataReads: ["weather/forecast"] makes the loader initialise this plugin AFTER weather. You don’t need loadAfter: ["weather"].
Cross-plugin reads are read-only. If you need another plugin to do something (not just read its data), use ctx.events.emit('your-plugin:topic', payload) and have the sibling plugin subscribe in its init().
3. Ad-hoc slash command
No schedule — the plugin only does something when the user types /sayhi in the chat.
// manifest.json
{
"id": "sayhi",
"name": "Sayhi Command",
"version": "1.0.0",
"description": "Adds a /sayhi command that replies with a greeting.",
"enabled": true,
"permissions": ["chat:send"],
"uses": ["chat"],
"commands": [
{ "id": "sayhi", "label": "Say hi", "args": [] }
]
}
// index.js
init(ctx) {
ctx.services.chat.onCommand('sayhi', async (chatId) => {
// Replying on the chat id the user spoke from is what `chat.send` is for.
await ctx.services.chat.send(chatId, 'Hi! 👋');
}, {
description: 'Reply with a friendly greeting.',
section: 'Personal',
trust: 'read',
});
},
Two halves that must agree: the commands entry in the manifest is the menu listing (it feeds slash-command autocomplete and the Commands tab in the desktop chat and Web Chat), and onCommand is the handler. The id has to match.
trust tells the assistant whether your command reads or changes something. Declare it: a command that doesn’t is guessed at from its name, and the guess is wrong in both directions. args is empty for a button-only command; add [{ name: 'amount', type: 'string', label: 'Amount', required: false }] to expose an input field. Only string args are supported today.
4. Polling an external API on an interval
Fetches data every N minutes, persists to the typed store, exposes via an HTTP route for a custom panel.
// manifest.json
{
"id": "stocks",
"name": "Stock Watch",
"version": "1.0.0",
"description": "Polls a stock-price API every 10 minutes.",
"enabled": true,
"apiPrefix": "/api/stocks",
"permissions": ["state:read", "state:write"],
"uses": [],
"config": {
"tickerList": { "type": "string", "default": "GOOGL,MSFT,AAPL",
"label": "Tickers (comma-separated)" },
"intervalSec": { "type": "number", "default": 600,
"label": "Poll interval (seconds)" }
}
}
// store/schema.js
'use strict';
module.exports = {
schema: {
quotes: {
primaryKey: 'symbol',
fields: {
symbol: 'string',
price: 'number',
delta: 'number',
at: 'string',
},
},
},
};
// index.js
'use strict';
const { log } = require('../../core/utils/logger');
module.exports = {
id: 'stocks',
init(ctx) {
const intervalMs = (ctx.pluginConfig.intervalSec || 600) * 1000;
ctx.schedule.every(intervalMs, async () => {
const tickers = String(ctx.pluginConfig.tickerList || '').split(',').map(s => s.trim()).filter(Boolean);
for (const symbol of tickers) {
try {
const res = await fetch(`https://your-quotes-api.example/v1/${symbol}`);
if (!res.ok) continue;
const { price, delta } = await res.json();
await ctx.store.set('quotes', {
symbol,
price,
delta,
at: new Date().toISOString(),
});
} catch (err) {
log(`[stocks] ${symbol}: ${err.message}`);
}
}
}, { tickId: 'stocks', runImmediately: true });
},
routes(router, ctx) {
router.get('/api/stocks/quotes', async ({ res }) => {
const rows = await ctx.store.get('quotes');
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ quotes: rows }));
});
},
};
Why use ctx.store: every quote write lands in data/quotes.json atomically, so a crash mid-write never produces a half-saved file. Other plugins can also read your quotes via ctx.data.read('stocks', 'quotes') if they declare it in their manifest.dataReads.
5. Reactive plugin — no schedule, only events
Subscribe to events emitted by another plugin and react to them. Pair with ctx.events.emit(...) in the publisher plugin.
// index.js
init(ctx) {
ctx.events.on('reminders:created', async (payload, ctxRef) => {
// payload is whatever the publisher sent
await ctx.plugins.api('agentum.chat-providers/delivery').send({
agentId: 'reminder-notifier',
channel: 'agentum-chat',
text: `📌 New reminder: ${payload.title}`,
});
});
},
Event handlers can also live in the handlers export, which the loader wires up automatically:
module.exports = {
id: 'reminder-notifier',
init(ctx) { /* … */ },
handlers: {
'reminders:created': async (payload, ctx) => {
await ctx.plugins.api('agentum.chat-providers/delivery').send(/* … */);
},
'reminders:completed': async (payload, ctx) => {
// …
},
},
};
Event names are conventionally <plugin-id>:<topic> so it’s clear which plugin owns each event.
6. Graceful-degradation pattern
A plugin that uses three integrations and one cross-plugin read but degrades to “do less” when any of them is unavailable:
init(ctx) {
ctx.schedule.dailyAt('08:00', async () => {
const lines = [];
// Cross-plugin data — guard on length
const forecast = ctx.data.read('weather', 'forecast');
if (forecast.length) {
lines.push(`☀️ ${forecast[0].summary}`);
}
// AI — guard on isReady (covers "not configured" + "user revoked")
if (ctx.services.ai.isReady()) {
const reflection = await ctx.services.ai.generate(
`Write a single warm sentence about starting the day well.`,
);
lines.push(reflection);
}
// Calendar — guard on isReady (the user might not have set up Calendar)
if (ctx.services.calendar.isReady()) {
const events = await ctx.services.calendar.fetchEvents();
const today = events.filter(e => isToday(e.start));
if (today.length) {
lines.push(`📅 ${today.length} event${today.length === 1 ? '' : 's'} today.`);
}
}
if (!lines.length) return;
// Delivery — NOT guarded. The user is always reachable, so a
// readiness check here would only ever suppress the message.
await ctx.plugins.api('agentum.chat-providers/delivery').send({
agentId: 'morning-frame',
channel: 'agentum-chat',
text: lines.join('\n'),
});
}, { timezone: 'America/Los_Angeles', tickId: 'morning-frame' });
},
function isToday(iso) {
if (!iso) return false;
const d = new Date(iso);
const now = new Date();
return d.getFullYear() === now.getFullYear()
&& d.getMonth() === now.getMonth()
&& d.getDate() === now.getDate();
}
The pattern: every external capability is wrapped in an isReady() check, and every data source is checked for length — so when the user configures or grants something new, the next tick picks up the improvement automatically. Delivery is the deliberate exception: it is never guarded, because there is always somewhere to put a message.
Where to next
- Authoring Agents — the full author’s reference (manifest fields, ctx surface, runtime panels, sharing).
- Using Agentum — the operator’s view (Integrations grants, the Schedule view, Agentum Chat).
- Authoring Kit: Claude Code & Codex — hand the full, current contract to your coding assistant.
- Troubleshooting — when an agent stops behaving.