IM Bot path: ship a bot without AI
This path builds a bot that receives a message, runs your logic, and replies or sends a notification on its own. It takes 45–60 minutes and needs no model or API key.
When you're done you'll have:
- a custom
/rollcommand that updates without a restart when you save; - a notification sent on a daily schedule;
- at least one real chat platform (QQ, Discord, Slack…) online.
1. Get it working in Sandbox
npm create zhin-app my-bot -y
cd my-bot
pnpm devSandbox is a built-in simulated chat window; it needs no platform account. Connect Console with the API Base printed in the terminal and HTTP_TOKEN from .env, open Sandbox under Conversations & Channels, send /hello, and check that it replies.
2. Add a command
Create commands/roll/index.ts:
import { defineCommand } from 'zhin.js/command';
export default defineCommand({
description: 'Roll a six-sided die',
execute: () => `🎲 ${1 + Math.floor(Math.random() * 6)}`,
});Save, then send /roll in Sandbox. You see the dice roll without restarting.
The command name comes from the file path: commands/roll/index.ts is /roll, and commands/gh/issue/index.ts is /gh issue. For parameters and permissions, see Commands.
3. Add a scheduled notification
The plugin entry below registers a job that runs at 9:00 every day. The scheduler only decides when; the message itself goes through the outbound service you get from outboundHostToken, so Sandbox and real platforms share one send path.
import {
definePlugin,
outboundHostToken,
scheduleHostToken,
} from 'zhin.js';
interface Config {
morningTarget?: {
adapter: string;
endpointKey: string;
conversation: { kind: 'private' | 'group' | 'channel'; id: string };
};
}
export default definePlugin<Config>({
name: 'my-bot',
setup(context) {
const target = context.config.get().morningTarget;
if (!target) return;
if (!context.resources.has(outboundHostToken)) return;
const outbound = context.resources.use(outboundHostToken);
const schedule = context.resources.use(scheduleHostToken);
context.lifecycle.add(schedule.register({
id: 'my-bot/morning',
cron: '0 0 9 * * *',
description: 'Send a morning greeting',
execute: async () => {
await outbound.send({
...target,
content: 'Good morning',
});
},
}));
},
});The job is registered only after you set morningTarget in config:
endpointKeyis the full Endpoint identity shown in Conversations & Channels (an Endpoint is one account the bot is signed in to on a platform);cronhas six fields: second, minute, hour, day, month, weekday;context.lifecycle.add()unregisters the old job on hot reload or when the plugin stops, so nothing is sent twice.
Why check resources.has(outboundHostToken) first?
Host tokens such as outboundHostToken and scheduleHostToken are services the runtime provides to plugins. A plugin can only use one when the current runtime installed it; checking first lets the plugin skip quietly where outbound delivery is unavailable (some tests, for example) instead of throwing. See Host capabilities.
4. Connect a real platform
npx zhin setup --adapters
pnpm install
pnpm devThe wizard asks which platform to connect, then updates package.json#zhin.plugins, zhin.config.yml, and .env together. Account secrets go only into .env; the config file keeps ${VAR} references.
The /roll command and the notification need no changes: commands don't depend on a specific platform, so moving from Sandbox to QQ, Discord, or Slack leaves your code as is. Per-platform setup is in Platform adapters.
5. Accept the result in Console
- Confirm the Endpoint and send a message in Conversations & Channels.
- In Runtime Capabilities, confirm
/rollis loaded and is the version you last saved. - Check Logs for command or notification failures.
- Restart the Host and repeat the real-platform test.
Common pitfalls
- Config in the wrong place:
zhin.config.ymlholds values only; which plugins are installed lives inpackage.json#zhin.plugins. - Bypassing the send path: send proactive messages through
outboundHostToken; don't keep an adapter instance and call its send method yourself. - A missing cron field: schedules need six fields with seconds first; a five-field expression such as
0 9 * * *is rejected.
Next steps
- AI Agent path: let the bot chat with a model and call tools
- Console path: manage several accounts and diagnose problems in the browser
- Commands: add parameters, permissions, and subcommands