Architecture
Akmatori runs as a small set of containers inside your network. An alert comes in as a webhook, an agent investigates it with the LLM you choose, and every tool call goes through one gateway that holds the credentials. This page shows the parts, how one incident moves through them, and how agents find their instructions and tools.
How one incident moves through the system
- An alert arrives. Alertmanager, Zabbix, PagerDuty, Grafana or Datadog posts to
/webhook/alert/{instance_uuid}. The proxy passes it to the API server, which checks the per-instance secret and normalizes the alert. - The API server opens an incident. It either attaches the alert to an open incident or creates a new one, writes
AGENTS.mdfor the run, and posts the alert to Slack, Telegram or Discord. - The agent worker gets the task. Over a WebSocket, the API server sends the incident, the skills, the LLM settings and the run's tool allowlist.
- The agent investigates. It runs the LLM loop against your provider. The worker is the only container that calls the LLM.
- Tools run in the gateway. Each
gateway_callgoes to the MCP Gateway, which checks the allowlist, loads the connector's credentials from PostgreSQL, and calls your system over SSH, SQL, HTTPS or MCP. - The answer goes back. Output streams to the API server, which saves the run and posts the root cause in the incident's chat thread.
- Memory is written after close. When an alert incident closes, a post-incident memory pass reads the runs and saves at most five graded, redacted memories for the next investigation.
Containers
The Docker Compose stack starts these services. Only the proxy publishes a port: ${HTTP_PORT:-8080}.
| Service | What it does |
|---|---|
akmatori-proxy | nginx edge. Routes /api/, /auth/ and /webhook/ to the API server and everything else to the web UI. The only published port. |
akmatori-frontend | nginx that serves the React web UI. |
akmatori-api | Go API server: REST with JWT auth, alert webhooks, Slack Socket Mode, Telegram and Discord, cron jobs, alert correlation and the post-incident memory pass. Writes AGENTS.md and SKILL.md for each run. |
akmatori-agent | Node agent worker: runs the LLM loop and execute_script. Receives tasks over a WebSocket from the API server and has no database credentials. |
akmatori-mcp-gateway | Go gateway that runs every tool call: checks the run allowlist, reads credentials from PostgreSQL at call time, and proxies remote MCP servers. |
akmatori-postgres | PostgreSQL 16: incidents, runbooks, memory, settings, LLM keys and connector credentials. |
akmatori-init | One-shot setup: creates data directories and the database password file, then exits. |
Instructions and tools
Agents read their instructions from files that Akmatori generates for each run. Tools are not assigned to skills: every enabled connector is available to an incident run, and the generated connected-tools skill lists them with their parameters and gateway_call(...) examples.
AGENTS.md
Incident-level instructions, written at the workspace root for each run.
- Defines the incident manager role and the investigation workflow
- Requires a runbook search, then a memory search, then a check of recent change events, before infrastructure tools
- Includes the global memory manifest and lists the uploaded context files
SKILL.md
A skill's prompt in the open Agent Skills format.
- YAML frontmatter and your own instructions
- The memory manifest for the skill's scope, most recently updated or used first
- A generated pointer: read
connected-tools/SKILL.mdbefore the first tool call
connected-tools/SKILL.md
A generated system skill. You cannot edit it.
- Every enabled tool instance with its logical name, parameters and examples
- The functions of HTTP connectors and remote MCP servers, read from the gateway registry
- Tells the agent to pass
logical_namewhen a tool type has more than one instance
Gateway tool registry
The tools that agents can call.
- Built-in types: SSH, Zabbix, VictoriaMetrics, Catchpoint, PostgreSQL, Grafana, PagerDuty, ClickHouse, NetBox, Kubernetes and Jira
- Credentialless namespaces:
events,incidents,proposals,runbooksandmemory - HTTP connectors add declarative API tools
- Remote MCP servers are proxied under a prefix such as
ext.github
## Production PostgreSQL (logical_name: "prod-db", type: postgresql)
Usage (via gateway_call):
```python
gateway_call("postgresql.list_tables", {}, "prod-db")
gateway_call("postgresql.describe_table", {"table_name": "users"}, "prod-db")
```
For multi-step work, use `execute_script` with built-in `gateway_call()`.
Tool authorization
A skill does not restrict tools. The API server builds each run's allowlist from the kind of run and sends it with the task, and the gateway refuses every tool outside it. To keep a connector away from agents, disable its tool instance.
| Run kind | Connector tools | Built-in tools |
|---|---|---|
| Incident (alert, Slack, API request) | All enabled | events, incidents, runbooks, memory |
| Proposal chat | None | incidents, proposals, runbooks, memory (memory writes refused) |
| Cron job, connector tools on | All enabled | The job's builtin_tools |
| Cron job, connector tools off | None | The job's builtin_tools |
Remote MCP servers
- Streamable HTTP only, with bearer, API-key header or OAuth 2.1 auth. OAuth tokens are stored in PostgreSQL.
- You can switch individual tools off. Tool schemas are refreshed every five minutes.
- Outbound connections refuse loopback, link-local and cloud-metadata addresses, and redirects are not followed. Set
MCP_BLOCK_PRIVATE_NETWORKSto refuse private ranges too. - A reload reconnects only the servers whose URL, prefix, auth or disabled tools changed.
Investigation guardrails
The incident manager prompt makes context recall a fixed first step. For every incident, the agent searches runbooks, then cross-incident memory, then recent change events, and only then calls infrastructure tools.
1. Understand the problem.
2. Search runbooks first:
gateway_call("runbooks.search", {"query": "<the original alert text>"})
then gateway_call("runbooks.get", {"id": <id>}) for the best match.
3. Search cross-incident memory next:
gateway_call("memory.search", {"query": "<the same query>", "scopes": ["<skill names relevant to this alert>", "global"]})
then gateway_call("memory.get", {"id": <id>}) for a relevant memory.
4. Check recent changes:
gateway_call("events.search", {"kind": "change", "since": "<60 min before the alert>", "host": "<host>"}, "events")
5. Read the relevant SKILL.md files and connected-tools/SKILL.md, then call infrastructure tools.
runbooks.search and memory.search use lexical (BM25) ranking with no LLM in the loop. The agent passes the original alert text as the query and may retry up to two times with a narrower query. A memory graded unverified is a lead to check against live data, not a fact. Runbooks and memory
Runbooks and memories live in PostgreSQL. Operators write runbooks and can add, edit or delete any memory in the UI. Agents use them through the runbooks.* and memory.* gateway tools.
- Alert investigations do not write memory. When an alert incident closes, the post-incident memory pass reads the runs, the alerts and the final answers, and saves at most five memories. A failed pass is retried up to three times with backoff, and you can run it again from the incident page.
- Other work can write memory directly. Request-driven runs save memories with
memory.write. - Evidence grades. Each memory is
confirmed(only an operator can set it),observed(seen directly) orunverified. An agent's causal claim is always unverified. - Protected rows. An agent cannot change operator memories or context-file summaries.
- Usage. Akmatori records which memories each run searched, read and cited. The Dreaming cron job can retire unverified agent memories that nobody used for 90 days.
- Secrets. Private keys, tokens, URL passwords and similar values are redacted on a best-effort basis before a memory is saved.
- From chat. In Slack, reply
remember: …in a thread to save an operator memory. You can turn memory generation off in Settings → Memory; operator writes still work.
Scheduled agents
Cron jobs use the same execution path as incidents. A cron job posts to its configured channel or falls back to the workspace default. Two fields control its tools: tools_enabled (all enabled connector tools, or none) and builtin_tools (which of events, incidents, proposals, runbooks and memory it can call). New cron jobs default to connector tools on, with events, runbooks and memory.
The self-improvement evaluator is a system cron job, off by default. It runs without connector tools and with the events, runbooks, memory, incidents and proposals tools, so it can review recent evidence and draft runbook and skill updates as proposals.
409 Conflict, so maintenance schedules survive routine cleanup. Design principles
Credentials stay in the gateway
Secrets live in PostgreSQL and are read by the MCP Gateway at call time. They never appear in prompts, skill files or agent output.
One path for every tool call
Agents call tools with gateway_call and execute_script, so authorization, credentials and auditing stay in one place.
Tool docs come from the running system
connected-tools/SKILL.md is built from the enabled tool instances, so the examples change when the tools change.
Recall before exploring
Runbooks, memory and recent changes come first, which removes avoidable exploration and leaves time for targeted debugging.
How the tool docs are generated
- You add, enable or disable a connector in the UI.
- The backend regenerates
connected-tools/SKILL.mdfrom every enabled tool instance, with its logical name and usage examples for its tool type. It also re-renders the file on a timer. - Every other
SKILL.mdkeeps its own instructions and ends with its memory manifest and a pointer toconnected-tools. - HTTP connector and MCP server changes trigger a gateway reload, so new tools appear without a rebuild.
- When an incident starts, Akmatori writes
AGENTS.mdwith the incident workflow, the global memory manifest and the list of context files.
Contributor checklist
Adding a built-in tool type
Register the tool in the gateway, add a usage example in generateToolUsageExample(), and add the type to hasHandWrittenToolExample(), so connected-tools/SKILL.md gets useful examples.
Changing how skill files are built
Keep the generated section markers and the stripping logic aligned, so your own prompt text is kept and the generated blocks stay current.
Extending integrations
Decide whether the new capability belongs in a built-in tool type, a declarative HTTP connector, or a proxied remote MCP server, and document that path.
Troubleshooting
The tool list looks wrong
Check which tool instances are enabled on the Connectors page; connected-tools/SKILL.md is rebuilt from them. At startup, Akmatori logs a warning for any skill, runbook, memory or cron prompt that still names the retired runbook or memory subagents. Rewrite those to use the gateway tools.
A connector or MCP server is missing
Check that the connector exists and is enabled, that a cron job that needs it has connector tools on, that its prefix does not clash with another, and that the connector page shows it as connected. Use Test connection to see the server's answer.
Agents explore too much
Name the right hosts and services in skill instructions, write runbooks for repeated alerts, and check that the generated examples are specific enough to run as they are.