Skip to content

The brain

The shared, stateless decision layer. It reads config from the registry, decides what should happen to a work item, and writes the decision to that tenant's database. It never executes anything itself.

Decided in ADR-0032. Engines and products in ADR-0002.

Shape

flowchart LR
    N8N["n8n<br/>tenant A"] -->|"tenant_id + work item<br/>+ latest result"| API["FastAPI"]
    subgraph BRAIN ["The brain: stateless"]
        API --> RES["Resolve config<br/>for this tenant"]
        RES --> G["LangGraph<br/>Capture · Progress<br/>Bill · Remember"]
    end
    REG[("Registry")] -.->|"read-only"| RES
    G -->|"per-tenant role"| TDB[("Tenant A<br/>Postgres")]
    G -->|"inference"| LLM["packages/llm-client"]
    G -->|"next action"| N8N
    classDef llm fill:#7c3aed,stroke:#5b21b6,color:#ffffff
    class LLM llm

The purple node marks a model call. Throughout these docs, purple means non-deterministic and billable. Everything else is code, a query, or a rule.

The loop

n8n drives, and the brain answers one question at a time.

sequenceDiagram
    participant N as n8n (tenant)
    participant B as Brain
    participant D as Tenant Postgres
    participant H as Human

    N->>B: work item + latest result
    B->>D: load state, config, turn count
    B->>B: decide
    B->>D: persist decision
    B-->>N: next action

    alt deterministic action
        N->>N: execute
        N->>B: outcome, loop continues
    else AI-generated action
        B->>D: write approval record
        Note over N: n8n STOPS. Nothing polls.
        H->>D: approve in the client review app
        D->>N: that write triggers the next run
        N->>N: execute
    end

Every turn is a fresh stateless call. State lives on the work item, so a restart mid-loop loses nothing, and either brain replica can answer the next turn (ADR-0030).

Approval is a stop, not a wait. A pending approval consumes nothing. The human's approval is itself the trigger for what happens next.

Three things that contain the damage

Identity comes from the credential, never the payload. Each tenant's n8n holds its own bearer token, and the brain resolves the tenant from the token. A tenant_id in the body is not trusted, and a body that disagrees with its token is rejected. Without this, the per-tenant database role below is decoration, because tenant A could simply ask for tenant B's data.

Actions come from a closed, versioned catalogue. The brain chooses from operations that already exist, with typed parameters, so the worst an LLM can decide is the wrong action from a known set rather than a novel one. Same reasoning as ADR-0004's structured predicates: no expression language, no eval, no improvisation. requires_approval is a property of the action type, not a judgement the model makes about itself.

Every decision carries an idempotency key, being work item plus turn number. n8n retries on timeout, and without a key a retry produces a second decision and a second draft, having already paid for both.

What it does and does not hold

Brain Where it lives instead
Loop state between turns No The work item, in tenant Postgres
LangGraph checkpoints No Tenant Postgres. Never local disk
Config Read-only, per request Registry (ADR-0006)
Client system credentials (Xero, AroFlo) Never n8n
Superuser DB access No Per-tenant role, resolved per request

The middle row is the one that breaks quietly. A checkpoint written to local disk is invisible on one host and corrupting on two, because a work item resumed on the other replica finds nothing (OI-049).

Data access, and the boundary it bends

This is the one place the isolation story is not absolute

ADR-0023 §6 rejected a shared Metabase because it would need credentials to every client database. The brain has exactly that property, and it is accepted rather than avoided.

A compromised brain crosses tenants. That is stated plainly rather than argued away.

Why it is accepted: it is our own code rather than a third-party product with a CVE history, it exposes no query interface, and the alternative (a brain per tenant) contradicts shared engines and multiplies maintenance by client count.

What reduces the risk:

  • Per-tenant database roles, resolved per request, with narrow grants. The container boundary protects tenants from each other, and this protects them from our bugs, which are far likelier than a compromise.
  • No client credentials in the brain. A compromise reaches our tenant databases and stops there. It cannot change anything in the client's actual systems of record.
  • Read-only against the registry. It cannot rewrite the map.

Loop termination

Every work item carries a turn counter with a low default ceiling. Reaching it halts the item and flags it for a human.

An agent cycling quietly is a cost failure and a correctness failure at once, so it must be visible on the work item rather than only in a log.

Version one

Scope
Engine Capture only. Progress in its most minimal form
Product Email triage
Tenant Client-zero, being Octopodia's own inbox
Autonomy Assist. Every draft approved, without exception
Not built Bill, Remember

Assist autonomy is not a soft start. There is no scored eval harness yet, so there is no evidence on which to grant more.

Stack

Python, FastAPI, LangGraph, Pydantic v2, following the pattern already proven in octopodia-api.

  • Every LLM output is parsed into a Pydantic model. Never a regular expression, never a bare json.loads.
  • LLM calls route through packages/llm-client (ADR-0009). No direct SDK calls, no OpenRouter.
  • Log token usage per request, never prompt content. Prompts carry client data.

Open

  • OI-049: statelessness must be verified when this is built, not assumed.
  • OI-050: the reconciliation pass must report brain version per host, so skew is visible.
  • No eval harness exists. Nothing currently measures decision quality, so "the brain works" today would mean "it returned valid JSON".