Event Sourcing & Review Process
Every agent action is an immutable event. No action is executed until reviewed and approved.
Why Event Sourcing
| Concern | Solution |
|---|---|
| Audit trail | Bokføringsloven requires 5-year retention. Every event is immutable, timestamped, and attributable to an actor. |
| Review before action | Agents propose, reviewers approve. No direct writes to Folio/Fiken. |
| Undo capability | Compensating events (reversal, correction) — never delete. |
| Replay | Rebuild state from events. Debug what happened and why. |
| Multi-agent coordination | Book-E proposes, Review-E verifies, system executes. Clear handoff. |
Event Flow
Book-E Event Store Review-E / Human Folio/Fiken
│ │ │ │
│ 1. Propose action │ │ │
│──────────────────────────▶│ │ │
│ │ 2. Store event │ │
│ │ (status: PROPOSED) │ │
│ │ │ │
│ │ 3. Notify reviewer │ │
│ │─────────────────────────▶│ │
│ │ │ │
│ │ 4. Approve/Reject │ │
│ │◀─────────────────────────│ │
│ │ │ │
│ │ 5. Store decision │ │
│ │ (status: APPROVED) │ │
│ │ │ │
│ │ 6. Execute action │ │
│ │─────────────────────────────────────────────────▶│
│ │ │ │
│ │ 7. Store result │ │
│ │ (status: EXECUTED) │ │
│ │ │ │
│ 8. Notify user │ │ │
│◀──────────────────────────│ │ │
Event Types
Receipt Events
ReceiptAttachmentProposed
→ actor: Book-E
→ data: {merchantName, amount, date, vatRate, accountCode, folioEventId, fileUrl}
→ confidence: 0.95
ReceiptAttachmentApproved
→ actor: Review-E | human
→ proposalId: <ref to proposed event>
→ corrections: {accountCode: "6540"} (optional — reviewer can fix values)
ReceiptAttachmentRejected
→ actor: Review-E | human
→ proposalId: <ref to proposed event>
→ reason: "Wrong merchant match"
ReceiptAttachmentExecuted
→ proposalId: <ref to proposed event>
→ approvalId: <ref to approved event>
→ folioResponse: {attachmentId, syncedToFiken: true}
Invoice Events
InvoiceRegistrationProposed
→ actor: Book-E
→ data: {supplier, amount, dueDate, vatRate, lineItems, fileUrl}
InvoiceRegistrationApproved
→ actor: Review-E | human
InvoiceRegistrationExecuted
→ fikenResponse: {purchaseId}
Expense Events
ExpenseRegistrationProposed
→ actor: Book-E
→ data: {employee, amount, category, vatRate, fileUrl}
ExpenseRegistrationApproved / Rejected / Executed
Pattern Events
PatternSuggested
→ actor: Book-E
→ data: {merchant, accountCode, vatRate}
→ reason: "New merchant, user confirmed in Discord thread"
PatternApproved → PatternApplied
Query Events (no review needed)
BalanceQueried
→ actor: Book-E
→ data: {accounts: [...]}
TransactionsQueried
→ actor: Book-E
→ data: {count, dateRange}
MissingReceiptsChecked
→ actor: CronJob
→ data: {count, events: [...]}
Event Schema (PostgreSQL)
CREATE TABLE events (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
type TEXT NOT NULL, -- e.g., 'ReceiptAttachmentProposed'
status TEXT NOT NULL DEFAULT 'PROPOSED', -- PROPOSED, APPROVED, REJECTED, EXECUTED, FAILED
actor TEXT NOT NULL, -- 'book-e', 'review-e', 'human:stig-johnny', 'system:cron'
data JSONB NOT NULL, -- event-specific payload
parent_id UUID REFERENCES events(id), -- links approval to proposal, execution to approval
correlation_id UUID NOT NULL, -- groups all events for one action
created_at TIMESTAMPTZ DEFAULT NOW(),
-- Immutable: no UPDATE or DELETE allowed
-- Use application-level policies or triggers
);
CREATE INDEX idx_events_type ON events(type);
CREATE INDEX idx_events_status ON events(status);
CREATE INDEX idx_events_correlation ON events(correlation_id);
CREATE INDEX idx_events_created ON events(created_at);
Review Rules
| Action | Auto-approve? | Why |
|---|---|---|
| Balance query | Yes (no review) | Read-only, no side effects |
| Transaction list | Yes (no review) | Read-only |
| Missing receipts check | Yes (no review) | Read-only |
| Receipt attachment (known pattern, high confidence) | Auto-approve | Pattern already verified by human |
| Receipt attachment (new merchant or low confidence) | Requires review | First time → human must verify account + VAT |
| Invoice registration | Requires review | Financial write — always verify |
| Expense registration | Requires review | Financial write — always verify |
| Pattern suggestion | Requires review (via PR) | Changes future automatic behavior |
Confidence Scoring
Book-E assigns confidence to each proposal:
| Confidence | Meaning | Review |
|---|---|---|
| > 0.9 | Known pattern, exact merchant match | Auto-approve |
| 0.7 — 0.9 | Likely match, similar merchant name | Review recommended |
| < 0.7 | New merchant or ambiguous | Review required |
Review-E Integration
Review-E already reviews code PRs. For accounting events:
- Book-E proposes action → event stored as PROPOSED
- API posts to Discord #review-e: "Book-E wants to attach receipt: Adobe 199 kr → konto 6540, 25% MVA"
- Review-E checks: amount matches, account code is reasonable, VAT rate correct
- Review-E approves or requests correction
- On approval → API executes the Folio/Fiken call
- On rejection → Book-E asks user for clarification
State Reconstruction
Current state is derived from events, not stored separately:
-- All pending proposals
SELECT * FROM events WHERE status = 'PROPOSED' ORDER BY created_at;
-- Full history of a transaction
SELECT * FROM events WHERE correlation_id = ? ORDER BY created_at;
-- All actions by Book-E today
SELECT * FROM events WHERE actor = 'book-e' AND created_at > NOW() - INTERVAL '1 day';
-- Approval rate
SELECT
COUNT(*) FILTER (WHERE status = 'APPROVED') as approved,
COUNT(*) FILTER (WHERE status = 'REJECTED') as rejected
FROM events WHERE type LIKE '%Proposed';