Skip to content

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:

  1. Book-E proposes action → event stored as PROPOSED
  2. API posts to Discord #review-e: "Book-E wants to attach receipt: Adobe 199 kr → konto 6540, 25% MVA"
  3. Review-E checks: amount matches, account code is reasonable, VAT rate correct
  4. Review-E approves or requests correction
  5. On approval → API executes the Folio/Fiken call
  6. 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';