Module Development
How to build your own module for Gumm. Modules are TypeScript packages that add tools the LLM can call automatically.
Prerequisites
- Basic TypeScript knowledge
- Understanding of async/await
- Access to the
modules/user/folder on your Gumm instance
Module structure
modules/user/my-tool/
├── manifest.json # Metadata (required)
├── index.ts # Logic: exports tools() and handler() (required)
└── ui.vue # Optional admin UI component
1. manifest.json
{
"id": "my-tool",
"name": "My Tool",
"version": "1.0.0",
"description": "Does something useful",
"entrypoint": "index.ts",
"capabilities": ["fetch"],
"defaultEnabled": false
}
| Field | Required | Description |
|---|---|---|
id | ✅ | Unique identifier, kebab-case |
name | ✅ | Display name in the UI |
version | — | Semver string (default 1.0.0) |
description | — | Short description shown in the UI |
entrypoint | — | Entry file (default index.ts) |
capabilities | — | Declared capabilities: ["fetch"], ["spotify"], etc. |
dependencies | — | IDs of other modules this module needs |
defaultEnabled | — | Whether the module starts enabled (default true) |
schedules | — | CRON jobs to register (see below) |
2. index.ts — Required exports
Your module must export two functions.
tools(): ToolDefinition[]
Returns OpenAI-compatible tool definitions. These are injected into the LLM payload when the module is active.
export function tools() {
return [
{
type: 'function',
function: {
name: 'weather_get',
description: 'Get the current weather for a city.',
parameters: {
type: 'object',
properties: {
city: {
type: 'string',
description: 'City name, e.g. "Paris"',
},
},
required: ['city'],
},
},
},
];
}
handler(toolName, args, ctx?): Promise<string>
Called when the LLM invokes one of your tools. Must return a JSON string.
export async function handler(
toolName: string,
args: Record<string, any>,
ctx?: any,
): Promise<string> {
if (toolName === 'weather_get') {
const response = await fetch(
`https://wttr.in/${encodeURIComponent(args.city)}?format=j1`,
);
const data = await response.json();
return JSON.stringify({
city: args.city,
temp_c: data.current_condition[0].temp_C,
description: data.current_condition[0].weatherDesc[0].value,
});
}
return JSON.stringify({ error: `Unknown tool: ${toolName}` });
}
3. Module Context (ctx)
When the Brain calls your handler, it injects a ModuleContext object with access to memory, storage, events, and configuration.
interface ModuleContext {
memory: NamespacedMemory; // scoped key-value memory for this module
storage: ModuleStorage; // timestamped records (like a mini database)
events: EventAccess; // emit and subscribe to events
modules: ModulesAccess; // call tools from other modules
brain: BrainAccess; // read brain config values
log: Logger; // structured logging
}
Reading configuration
If your module needs credentials (API keys, tokens), store them in brain config via ctx.brain.getConfig(). This keeps all credentials in the dashboard — no env vars.
const apiKey = await ctx.brain.getConfig('api.my-tool.apiKey');
Users set this value in the APIs section (/apis) or directly in Brain config.
Using memory
// Save something
await ctx.memory.remember('last_city', args.city);
// Read it back later
const lastCity = await ctx.memory.recall('last_city');
Emitting events
await ctx.events.emit('weather.fetched', { city: args.city });
4. CRON schedules
Modules can declare scheduled tasks in the manifest:
{
"schedules": [
{
"name": "daily-update",
"cron": "0 8 * * *",
"handler": "daily_briefing",
"description": "Send a daily briefing at 8am"
}
]
}
The handler function is exported from index.ts:
export async function daily_briefing(
args: Record<string, any>,
ctx?: any,
): Promise<string> {
// This runs every day at 8am
return JSON.stringify({ message: 'Good morning!' });
}
5. Optional: ui.vue admin panel
If your module needs configuration (API keys, OAuth, preferences), add a ui.vue file. It renders in the module’s card in the dashboard.
<template>
<div class="p-4 space-y-4">
<label class="block text-sm font-medium">API Key</label>
<input
v-model="apiKey"
type="password"
class="input w-full"
placeholder="Enter your API key"
/>
<button @click="save" class="btn btn-primary">Save</button>
</div>
</template>
<script setup>
import { ref } from 'vue';
const apiKey = ref('');
async function save() {
await $fetch('/api/brain/config', {
method: 'PUT',
body: { entries: [{ key: 'api.my-tool.apiKey', value: apiKey.value }] },
});
}
</script>
6. Deploying your module
Drop the folder into modules/user/ on your server:
# If your server is accessible
scp -r my-tool user@server:/path/to/gumm/modules/user/
# Or if your server is on a VPN
rsync -av my-tool/ user@100.x.y.z:/path/to/gumm/modules/user/my-tool/
The Module Registry detects the new folder within seconds and loads it automatically. Check the Modules page in the dashboard to verify.
To share your module with others, push it to GitHub and install it via:
gumm modules install your-username/gumm-my-tool
Best practices
- Keep
index.tsunder 300 lines; split into sub-files if it grows larger - Return clear, structured JSON from handlers — the LLM reads this output
- Never hardcode credentials — use
ctx.brain.getConfig()and guide the user to set them in/apis - Always handle the
defaultcase in yourhandlerswitch with a clear error message - Test your handler locally before deploying — it’s just a TypeScript function