Conversation Agent — Powered by OpenClaw
What It Is
The Conversation Agent is an OpenClaw agent — the same framework that powers our internal agents (iClaw-E, Pi-E, Volt-E). OpenClaw already handles conversation management, memory, persona, and tool execution. We extend it with channel adapters (Slack, Teams, Web) and register the Accounting API as tools.
Why OpenClaw
| Need | OpenClaw | Build from scratch |
|---|---|---|
| Conversation loop | Built in | Weeks to build |
| Memory/context | Built in | Build or integrate |
| Persona/character | Built in (config) | Build |
| Tool execution | Built in | Build |
| Discord | Built in | Build |
| Slack/Teams/Web | Needs adapters | Build |
| Production-tested | Running our agents now | Untested |
| We know it | Yes | N/A |
Estimated work: - OpenClaw with Accounting API tools: days - New conversation agent from scratch: weeks
Architecture
┌────────────────────────────────────────────────────┐
│ OpenClaw Agent │
│ │
│ ┌─────────────────────────────────────────────┐ │
│ │ Channel Adapters │ │
│ │ Discord (built in) │ │
│ │ Slack (new adapter) │ │
│ │ Teams (new adapter) │ │
│ │ Web (new adapter — WebSocket/REST) │ │
│ │ Email (new adapter — webhook) │ │
│ └──────────────────┬──────────────────────────┘ │
│ │ │
│ ┌──────────────────▼──────────────────────────┐ │
│ │ OpenClaw Core │ │
│ │ • Conversation loop (multi-turn) │ │
│ │ • Memory (structured facts per user) │ │
│ │ • Persona ("Regn" — friendly accountant) │ │
│ │ • LLM (Gemini Flash / Claude) │ │
│ │ • Tool router (function-calling) │ │
│ └──────────────────┬──────────────────────────┘ │
│ │ │
│ ┌──────────────────▼──────────────────────────┐ │
│ │ Registered Tools │ │
│ │ │ │
│ │ accounting_solve(task, files) │ │
│ │ accounting_query(question) │ │
│ │ accounting_monitor(check_type) │ │
│ │ conversation_facts(user_id) │ │
│ │ rules_check(entity_type, data) │ │
│ │ │ │
│ │ Each tool = HTTP call to Accounting API │ │
│ └──────────────────┬──────────────────────────┘ │
└─────────────────────┼──────────────────────────────┘
│ HTTPS
┌─────────────────────▼──────────────────────────────┐
│ Accounting API (ASP.NET Core 10) │
└─────────────────────────────────────────────────────┘
OpenClaw Configuration
{
"agent": {
"name": "Regn",
"persona": "Du er Regn, en vennlig og profesjonell AI-regnskapsfører for norske bedrifter. Du svarer på norsk med mindre brukeren skriver på et annet språk.",
"model": "gemini-2.5-flash",
"temperature": 0.3,
"maxTurns": 10
},
"channels": {
"discord": {
"enabled": true,
"guildId": "...",
"channelIds": ["regnskap", "expenses"]
},
"slack": {
"enabled": true,
"botToken": "xoxb-...",
"appToken": "xapp-..."
},
"teams": {
"enabled": false
},
"web": {
"enabled": true,
"websocketPath": "/ws/chat"
},
"email": {
"enabled": true,
"webhookPath": "/webhook/email"
}
},
"tools": {
"accountingApiBase": "https://api.ai-accountant.no",
"auth": "service-jwt",
"timeout": 30
},
"memory": {
"type": "structured_facts",
"maxFactsPerUser": 50,
"storageVia": "accounting_api"
}
}
OpenClaw Tools
The Accounting API endpoints are registered as OpenClaw tools. The LLM decides which tool to call based on the conversation. Every tool is an HTTP call to the Accounting API — OpenClaw never accesses the database directly.
Accounting Tools (call Accounting API)
accounting_solve
Execute an accounting task (invoice, payment, expense, etc.).
| Parameter | Type | Required | Description |
|---|---|---|---|
task |
string | Yes | Natural-language task description |
files |
string[] | No | File IDs of uploaded documents (receipts, invoices) |
Returns: { status: "completed" | "error", summary: string, entities_created: object[], reversible: boolean }
Example:
{
"task": "Register taxi receipt, employee Lars, dept Salg",
"files": ["abc123"]
}
API endpoint: POST /solve
accounting_query
Read-only data query. No side effects, no confirmation required.
| Parameter | Type | Required | Description |
|---|---|---|---|
question |
string | Yes | Natural-language question about accounting data |
Returns: { data: object, summary: string }
Example:
{
"question": "What is Kari Nordmann's remaining travel budget for 2026?"
}
API endpoint: POST /query
accounting_monitor
Run a compliance or monitoring check.
| Parameter | Type | Required | Description |
|---|---|---|---|
check_type |
string | Yes | Type of check: overdue_bills, expense_compliance, month_end_status, payroll_anomalies, accounting_violations |
Returns: { violations: object[], summary: string, severity: "info" | "warning" | "critical" }
Example:
{
"check_type": "overdue_bills"
}
API endpoint: POST /monitor
rules_check
Validate data against company rules before executing an action.
| Parameter | Type | Required | Description |
|---|---|---|---|
entity_type |
string | Yes | What is being validated: expense, invoice, payment, payroll |
data |
object | Yes | The entity data to validate |
Returns: { result: "PASS" | "WARN" | "FAIL", reasons: string[], suggested_account: number? }
Example:
{
"entity_type": "expense",
"data": {"category": "taxi", "amount": 1200, "has_receipt": false}
}
API endpoint: POST /rules/check
Memory Tools (call Accounting API)
get_user_facts
Get structured facts for a user (department, role, pending files, preferences).
| Parameter | Type | Required | Description |
|---|---|---|---|
user_id |
string | Yes | User email address |
Returns: { user: string, company: string, facts: object }
Example:
{
"user_id": "lars@firma.no"
}
API endpoint: GET /conversation/facts?user_id={user_id}
update_user_facts
Update structured facts after a conversation (e.g., clear pending files, update last action).
| Parameter | Type | Required | Description |
|---|---|---|---|
user_id |
string | Yes | User email address |
facts |
object | Yes | Facts to update (merged with existing) |
Returns: { updated: true }
Example:
{
"user_id": "lars@firma.no",
"facts": {"last_action": "taxi 350kr registered", "pending_files": []}
}
API endpoint: POST /conversation/facts
get_company_context
Get company information — departments, employees, chart of accounts, cached context.
| Parameter | Type | Required | Description |
|---|---|---|---|
company_id |
string | Yes | Company identifier |
Returns: { company: object, departments: object[], employees: object[], accounts: object[] }
Example:
{
"company_id": "invotek-as"
}
API endpoint: GET /company/context?company_id={company_id}
Notification Tools
send_message
Send a message to a specific user on a specific channel.
| Parameter | Type | Required | Description |
|---|---|---|---|
channel |
string | Yes | Channel type: slack, discord, email, web |
user_id |
string | Yes | User identifier (email or channel-specific ID) |
message |
string | Yes | Message content (markdown supported) |
Returns: { sent: true, message_id: string }
API endpoint: POST /notifications/send
send_approval_request
Send an approval request to a manager or accountant.
| Parameter | Type | Required | Description |
|---|---|---|---|
approver_id |
string | Yes | Email of the person who should approve |
task |
string | Yes | Description of what needs approval |
data |
object | Yes | The data being approved (expense, invoice, etc.) |
Returns: { request_id: string, sent: true }
API endpoint: POST /notifications/approval
schedule_reminder
Schedule a future reminder for a user.
| Parameter | Type | Required | Description |
|---|---|---|---|
user_id |
string | Yes | User email address |
message |
string | Yes | Reminder message |
when |
datetime | Yes | ISO 8601 datetime for when to send |
Returns: { reminder_id: string, scheduled_for: datetime }
API endpoint: POST /notifications/reminder
File Tools
upload_file
Store a file for processing (receipt photo, PDF invoice, bank statement CSV).
| Parameter | Type | Required | Description |
|---|---|---|---|
content |
bytes | Yes | File content (base64-encoded) |
filename |
string | Yes | Original filename with extension |
Returns: { file_id: string, mime_type: string, size_bytes: number }
API endpoint: POST /files/upload
get_file_text
Get OCR/extracted text from a previously uploaded file.
| Parameter | Type | Required | Description |
|---|---|---|---|
file_id |
string | Yes | File ID from upload_file |
Returns: { text: string, confidence: number, fields: object? }
API endpoint: GET /files/{file_id}/text
Tool Summary
| Tool | API Endpoint | Purpose |
|---|---|---|
accounting_solve |
POST /solve |
Execute accounting task |
accounting_query |
POST /query |
Read-only data query |
accounting_monitor |
POST /monitor |
Run compliance check |
rules_check |
POST /rules/check |
Validate against company rules |
get_user_facts |
GET /conversation/facts |
Get user's structured facts |
update_user_facts |
POST /conversation/facts |
Update user's facts |
get_company_context |
GET /company/context |
Get company info and cached data |
send_message |
POST /notifications/send |
Send message to channel |
send_approval_request |
POST /notifications/approval |
Request approval |
schedule_reminder |
POST /notifications/reminder |
Schedule future reminder |
upload_file |
POST /files/upload |
Store file for processing |
get_file_text |
GET /files/{file_id}/text |
Get OCR/extracted text |
The LLM sees these as functions. OpenClaw handles the HTTP serialization.
Conversation Flow — Step by Step
Example: "Kan du registrere taxikvitteringen jeg sendte i går?"
Turn 1: Message arrives via Slack adapter
OpenClaw loads user facts:
→ tool_call: conversation_facts(user_id="lars@firma.no")
→ returns: {pending_files: [{yesterday, "taxi_receipt.jpg", id: "abc123"}]}
Turn 2: OpenClaw has context
LLM thinks: "Found yesterday's receipt. Send to accounting."
→ tool_call: accounting_solve(
task="Register taxi receipt, employee Lars, dept Salg",
files=["abc123"]
)
→ returns: {status: "completed", summary: "Taxi 350 kr, konto 7140"}
Turn 3: Update facts and reply
→ tool_call: conversation_facts(user_id="lars@firma.no",
update={remove_pending: "abc123", last_action: "taxi 350kr registered"})
→ Slack reply: "Registrert! Taxi 350 kr på konto 7140, avd. Salg."
Example: Clarification needed
Turn 1: "Lag faktura til Nordvik"
LLM thinks: "Need more info. Let me check if there are multiple Nordviks."
→ tool_call: accounting_query(question="Search customers named Nordvik")
→ returns: [{name:"Nordvik AS"}, {name:"Nordvik Shipping AS"}]
Turn 2: Ask user
Slack reply: "Hvilken Nordvik?
1. Nordvik AS (912345678)
2. Nordvik Shipping AS (987654321)
Hva gjelder fakturaen og hvor mye?"
Turn 3: "Nr 1, konsulentbistand 25000"
→ tool_call: accounting_solve(
task="Invoice Nordvik AS (912345678), 25000kr, Konsulentbistand")
→ Slack reply: "Faktura #1234 opprettet. Skal jeg sende den?"
Turn 4: "Ja"
→ tool_call: accounting_solve(task="Send invoice #1234")
→ Slack reply: "Sendt!"
Memory: Structured Facts
OpenClaw stores structured facts per user, not raw chat logs:
{
"user": "lars@firma.no",
"company": "invotek-as",
"facts": {
"department": "Salg",
"role": "Konsulent",
"language": "no",
"pending_files": [],
"last_action": "taxi 350kr registered 2026-03-22",
"waiting_for": null,
"travel_budget_used": 12450,
"travel_budget_total": 30000,
"preferences": {
"tone": "informal",
"auto_send_invoices": false
}
}
}
Facts are updated after each conversation via the conversation_facts tool. They're stored in the Accounting API's database — OpenClaw never touches the DB directly.
Channel Adapters to Build
| Channel | Status | Work |
|---|---|---|
| Discord | Built in | None |
| Slack | New | Slack Bolt SDK, event subscriptions, file upload |
| Teams | New | Bot Framework SDK, Teams manifest |
| Web | New | WebSocket endpoint, simple React chat widget |
| New | Webhook receiver, MIME parser, reply via SMTP |
MVP: Discord (free) + Slack (most companies use it). Teams and email in Phase 2.
Scaling
OpenClaw instances are stateless (memory stored via Accounting API). Run N instances behind a load balancer:
Slack ─┐
Teams ─┤
Discord─┼→ Load Balancer → OpenClaw (N instances) → Accounting API
Web ─┤ ↓
Email ─┘ Database