Apus is a self-hosted gateway that connects Discord, Google Chat, iMessage, Matrix, Microsoft Teams, Signal, Slack, Telegram, WhatsApp, Zalo, and more to AI agents. This guide covers the "personal assistant" setup: a dedicated WhatsApp number that behaves like your always-on AI assistant. Setting up a shared gateway for several people instead? See Team setup.
Good defaults first
A connected agent is a capable one: depending on your tool policy it can run commands, work with files in its workspace, and message people on your behalf. The defaults keep that power scoped to you; a few settings are worth confirming up front:
- Always set
channels.whatsapp.allowFrom(never run open-to-the-world on your personal Mac). - Use a dedicated WhatsApp number for the assistant.
- Heartbeats default to every 30 minutes. Set
agents.defaults.heartbeat.every: "0m"to disable recurring polling while you evaluate the setup. Targeted event-driven follow-ups can still run, so keep tool policy and sandboxing conservative until you trust the setup.
Prerequisites
- Apus installed and onboarded - see Getting Started if you haven't done this yet
- The WhatsApp plugin installed, or WhatsApp chosen during onboarding. WhatsApp is an official plugin that installs on demand. See WhatsApp
- A second phone number (SIM/eSIM/prepaid) for the assistant
The two-phone setup (recommended)
You want this:
flowchart TB
A["<b>Your Phone (personal)<br></b><br>Your WhatsApp<br>+1-555-YOU"] -- message --> B["<b>Second Phone (assistant)<br></b><br>Assistant WA<br>+1-555-ASSIST"]
B -- linked via QR --> C["<b>Your Mac (apus)<br></b><br>AI agent"]
If you link your personal WhatsApp to Apus, every message to you becomes "agent input". That's rarely what you want.
5-minute quick start
- Pair WhatsApp Web (shows QR; scan with the assistant phone):
apus channels login
- Start the Gateway (leave it running):
apus gateway --port 18789
- Put a minimal config in
~/.apus/apus.json:
{
gateway: { mode: "local" },
channels: { whatsapp: { allowFrom: ["+15555550123"] } },
}
Now message the assistant number from your allowlisted phone.
When onboarding finishes, Apus auto-opens the dashboard and prints a clean (non-tokenized) link. If the dashboard prompts for auth, paste the configured shared secret into Control UI settings. Onboarding uses a token by default (gateway.auth.token), but password auth works too if you switched gateway.auth.mode to password. To reopen later: apus dashboard.
Give the agent a workspace (AGENTS)
Apus reads operating instructions and "memory" from its workspace directory.
By default, Apus uses ~/.apus/workspace as the agent workspace, and creates it (plus starter AGENTS.md, SOUL.md, IDENTITY.md, USER.md) automatically on onboarding or first agent run. Put environment-specific tool notes in the ## Tools section of AGENTS.md. BOOTSTRAP.md is only created for a brand-new workspace and should not come back after you delete it. MEMORY.md is optional and never auto-created; when present, it loads for normal sessions. Subagent sessions only inject AGENTS.md.
Treat this folder like Apus's memory and make it a git repo (ideally private) so your AGENTS.md and memory files are backed up. If git is installed, brand-new workspaces are auto-initialized with git init.
To create the workspace and config folders without running the full onboarding wizard:
apus setup --baseline
(Bare apus setup is an alias for apus onboard and runs the full interactive wizard.)
Full workspace layout + backup guide: Agent workspace Memory workflow: Memory
Optional: choose a different workspace with agents.defaults.workspace (supports ~).
{
agents: {
defaults: {
workspace: "~/.apus/workspace",
},
},
}
If you already ship your own workspace files from a repo, you can disable bootstrap file creation entirely:
{
agents: {
defaults: {
skipBootstrap: true,
},
},
}
The config that turns it into "an assistant"
Apus defaults to a good assistant setup, but you'll usually want to tune:
- persona/instructions in
SOUL.md - thinking defaults (if desired)
- heartbeats (once you trust it)
Example:
{
logging: { level: "info" },
agents: {
defaults: {
workspace: "~/.apus/workspace",
thinkingDefault: "high",
timeoutSeconds: 1800,
// Start with 0; enable later.
heartbeat: { every: "0m" },
},
entries: {
main: {
groupChat: {
mentionPatterns: ["@apus", "apus"],
},
},
},
},
channels: {
whatsapp: {
allowFrom: ["+15555550123"],
groups: {
"*": { requireMention: true },
},
},
},
session: {
scope: "per-sender",
resetTriggers: ["/new", "/reset"],
reset: {
mode: "daily",
atHour: 4,
idleMinutes: 10080,
},
},
}
Sessions and memory
- Session rows, transcript rows, and metadata (token usage, last route, etc):
~/.apus/agents/<agentId>/agent/apus-agent.sqlite - Legacy/archive transcript artifacts:
~/.apus/agents/<agentId>/sessions/ - Legacy row migration source:
~/.apus/agents/<agentId>/sessions/sessions.json /newor/resetstarts a fresh session for that chat (configurable viasession.resetTriggers). If sent alone, Apus acknowledges the reset without invoking the model./compact [instructions]compacts the session context and reports the remaining context budget.
Heartbeats (proactive mode)
By default, Apus runs a heartbeat every 30 minutes — or every hour when Anthropic OAuth/token auth is configured (including Claude CLI reuse). See Heartbeat for the full defaults. The prompt is:
Follow the heartbeat monitor scratch context when provided. Recurring tasks are automations; create or change their schedules with the automations tool, not heartbeat scratch. Do not infer or repeat old tasks from prior chats. If nothing needs attention, reply NO_REPLY.
Set agents.defaults.heartbeat.every: "0m" to disable recurring cadence. Targeted event-driven wakes, such as background exec completion follow-ups, remain available and do not create a recurring schedule. Heartbeat checklists live in the monitor's cron scratch (see Heartbeat); apus doctor --fix migrates a legacy workspace HEARTBEAT.md into it.
- If the monitor scratch exists but is effectively empty (only blank lines, Markdown/HTML comments, Markdown headings like
# Heading, fence markers, or empty checklist stubs), Apus skips the heartbeat run to save API calls. - If no scratch exists, the heartbeat still runs and the model decides what to do.
- If the agent replies with
NO_REPLY, Apus suppresses outbound delivery for that heartbeat. LegacyHEARTBEAT_OKreplies remain supported with a fixed 300-character acknowledgment budget. - By default, heartbeat delivery to DM-style
user:<id>targets is allowed. Setagents.defaults.heartbeat.directPolicy: "block"to suppress direct-target delivery while keeping heartbeat runs active. - Heartbeats run full agent turns - shorter intervals burn more tokens.
{
agents: {
defaults: {
heartbeat: { every: "30m" },
},
},
}
Media in and out
Inbound attachments (images/audio/docs) can be surfaced to your command via templates:
{{AttachmentPath}}(local temp file path){{AttachmentUrl}}(original URL or provider reference){{AttachmentContentType}}(MIME content type){{AttachmentDir}}(directory containing the local path){{AttachmentIndex}}(zero-based source fact index){{Transcript}}(if audio transcription is enabled)
The older {{MediaPath}}, {{MediaUrl}}, {{MediaType}}, and {{MediaDir}}
names remain available as deprecated compatibility aliases.
Outbound attachments from the agent use structured media fields on the message tool or reply payload, such as media, mediaUrl, mediaUrls, path, or filePath. Example message-tool arguments:
{
"message": "Here's the screenshot.",
"mediaUrl": "https://example.com/screenshot.png"
}
Apus sends structured media alongside the text. Legacy final assistant replies may still be normalized for compatibility, but tool output, browser output, streaming blocks, and message actions do not parse text as attachment commands.
If you must use a legacy final-reply MEDIA: line, keep it as standalone plain
text. Markdown wrappers, code fences, and inline prose such as
**MEDIA:/path.png**, `MEDIA:/path.png`, or
Here is the image: MEDIA:/path.png stay text and do not attach media. See
Rich output protocol.
Local-path behavior follows the same file-read trust model as the agent:
- If
tools.fs.workspaceOnlyistrue, outbound local media paths stay restricted to the Apus temp root, the media cache, agent workspace paths, and files generated in the active session sandbox. Files in sibling sandboxes remain inaccessible. - If
tools.fs.workspaceOnlyisfalse, outbound local media can use host-local files the agent is already allowed to read. - Local paths can be absolute, workspace-relative, or home-relative with
~/. - Host-local sends still only allow media and supported document types (images, audio, video, PDF, Office documents including macro-enabled Excel
.xlsm, and validated text documents such as Markdown/MD, TXT, JSON, YAML, and YML). This is an extension of the existing host-read trust boundary, not a secret scanner: if the agent can read a host-localsecret.txtorconfig.json, it can attach that file when the extension and content validation match. File-type validation does not establish that an attached workbook's macros are safe to run.
Keep sensitive files outside the agent-readable filesystem, or keep tools.fs.workspaceOnly: true for stricter local-path sends.
When the message tool cannot stage an attachment for the current conversation, its error identifies the file and the reported reason, such as a missing file or an unsupported local format. Being inside the workspace does not make every file type eligible for host-local attachment reads.
Operations checklist
apus status # local status (creds, sessions, queued events)
apus status --all # full diagnosis (read-only, pasteable)
apus status --deep # check channels (WhatsApp Web + Telegram + Discord + Slack + Signal)
apus health --json # gateway health snapshot over the WS connection
Logs live under /tmp/apus/: apus-YYYY-MM-DD.log for the default
profile and apus-<profile>-YYYY-MM-DD.log for named profiles.
Next steps
- WebChat: WebChat
- Gateway ops: Gateway runbook
- Cron + wakeups: Cron jobs
- macOS menu bar companion: Apus macOS app
- iOS node app: iOS app
- Android node app: Android app
- Windows Hub: Windows
- Linux status: Linux app
- Security: Security