Agent DB Schema
Status
Draft canonical spec. Companion to Synedrix OS - Agent Spec and all agent-layer sub-specs. Supersedes: v1.3. Related artifacts: Synedrix OS - Agent Spec, Synedrix OS - Agent Registry Spec, Synedrix OS - Agent Runtime Policy Spec, Synedrix OS - Agent Evaluation Spec, Synedrix OS Tech Stack, Synedrix OS Observability Metrics Spec.
Purpose
This document defines the canonical relational database schema for the Synedrix OS agent layer. All tables live in Supabase Postgres (PG17) as the canonical transactional truth layer. Hot execution state lives in Redis and is not defined here. Trace and LLM evaluation state lives in Langfuse and is referenced, not owned, by this schema. Behavioural analytics live in PostHog.
The schema follows the Synedrix OS principle of separating kinds of truth: static definition state, runtime invocation records, evaluation records, and lifecycle audit logs are distinct table groups with different access patterns and retention policies.
Observability Fields
Metric fields for invocation outcomes, tool_invocations, conversation_metrics, response_reviews, and guardrail_metrics are defined in Synedrix OS Observability Metrics Spec. That spec is the normative owner of all metric field names, types, and null-handling rules for those tables. Invocation identity and start-time fields belong to agent_invocations; terminal outcome and metric fields belong to agent_invocation_outcomes. This schema document governs table grouping, foreign key structure, RLS policy requirements, and retention policy. Domain packs MUST NOT add columns to these OS-owned tables from a domain context; metric schema changes must go through the observability spec versioning process.
Design principles
- Immutable audit records — invocation starts, terminal outcomes, tool-call records, lifecycle events, and compliance breach records are append-only and never updated. Only the retention role may delete eligible invocation starts, outcomes, and tool-call records under the configured retention and offboarding policies.
- Normalized over denormalized — agent sub-objects (prompt layers, tool bindings, memory profiles, etc.) live in separate tables with foreign keys rather than as JSONB blobs. JSONB is reserved for user-defined schemas (e.g. task input/output payloads) where the structure is not controlled by the OS.
- Versioned definitions — every agent definition change creates a new version row; current version is tracked by pointer in the parent record.
- Tenant-scoped controls — business instance overrides and enable/disable controls are isolated in their own tables and never merged into the core definition tables.
- Tenant boundary — every table is classified as reference, mixed or tenant-owned (see Tenant boundary). Tenant-owned and mixed tables carry
tenant_id; the tenant for a request comes from the credential, never from request input (Tenancy Spec). - Supabase Row Level Security (RLS) — every table must have RLS policies enforcing tenant, domain-pack and business-instance scoping, in that order, and row-level security is forced so a table owner cannot bypass it.
Table groups
Group A — Agent Definition (static, versioned)
agent_definitions
agent_versions
agent_prompt_layers
agent_authority_profiles
agent_contracts
agent_tool_bindings
agent_memory_profiles
agent_task_surfaces
agent_workflow_roles
agent_interop_profiles
agent_policy_bindings
Group B — Registry & Lifecycle
agent_registry_entries
agent_registry_versions
agent_capability_tags
agent_dependency_declarations
agent_instance_controls
agent_lifecycle_events
Group C — Runtime Policy
agent_runtime_policies
agent_runtime_policy_versions
agent_model_routing_rules
agent_circuit_breaker_state
agent_runtime_events
Group D — Execution Records (append-only)
agent_invocations
agent_invocation_outcomes
workflow_outcomes
agent_tool_call_records
agent_tool_call_results
workflow_task_assignments
Group E — Evaluation & Quality
agent_eval_suites
agent_eval_runs
agent_eval_results
agent_eval_promotion_gates
agent_eval_compliance_events
Group F — Lineage & Graph (synced to Neo4j)
agent_lineage_links
Group G — Memory Control Plane
memory_records
memory_record_lineage
memory_promotions
memory_tombstones
memory_recall_log
Group H — Identity & Access
organisations
users
role_assignments
authority_delegations
approval_decisions
role_assignment_events
service_principals
(normative DDL in Identity Spec v0.3 § Persistence; listed here for inventory and tenant class)The core entities and how the groups relate, reduced to an identifying key and a status, with no column lists:
erDiagram
AGENT_DEFINITION ||--o{ AGENT_VERSION : "has versions"
AGENT_DEFINITION ||--o| AGENT_REGISTRY_ENTRY : "is registered as"
AGENT_REGISTRY_ENTRY ||--|{ AGENT_REGISTRY_VERSION : "has history"
AGENT_DEFINITION ||--o{ AGENT_LIFECYCLE_EVENT : "is audited by"
AGENT_DEFINITION ||--o{ AGENT_RUNTIME_POLICY : "is governed by"
AGENT_DEFINITION ||--o{ AGENT_INVOCATION : "is invoked as"
AGENT_INVOCATION ||--o| AGENT_INVOCATION_OUTCOME : "ends in"
AGENT_INVOCATION ||--o{ AGENT_TOOL_CALL_RECORD : "makes"
AGENT_DEFINITION ||--o{ AGENT_EVAL_SUITE : "is evaluated by"
AGENT_EVAL_SUITE ||--o{ AGENT_EVAL_RUN : "is run as"
AGENT_EVAL_RUN ||--o{ AGENT_EVAL_RESULT : "records"
AGENT_DEFINITION ||--o{ WORKFLOW_TASK_ASSIGNMENT : "is assigned by"
MEMORY_RECORD ||--o{ MEMORY_PROMOTION : "is promoted by"
MEMORY_RECORD ||--o| MEMORY_TOMBSTONE : "is erased to"
AGENT_DEFINITION {
string tenant_id
string agent_id
}
AGENT_INVOCATION {
string tenant_id
string invocation_id
}
AGENT_INVOCATION_OUTCOME {
string outcome_id
string status
}
MEMORY_RECORD {
string tenant_id
string memory_id
}
Tenant boundary
Implements ADR-OS-008 and the Tenancy Spec. This section is normative for the table definitions below.
Table classes
| Class | Meaning | tenant_id |
|---|---|---|
| Reference | Definitions shared by every tenant. Read-only to tenant credentials, writable only by a platform or pack-publishing role | Absent |
| Mixed | A table holding both shared definitions and tenant-contributed ones, such as the agents a Business Instance Extension registers | Non-null. The reserved value platform marks a shared row; any other value is the owning tenant |
| Tenant-owned | Records that belong to one tenant | Non-null, never platform |
platform is a reserved scope value. It cannot be issued as a tenant_id. Using a reserved value instead of NULL keeps tenant_id part of every primary key and unique key, which a NULL could not be, and lets a plain foreign key express the scope of a reference.
Rules
Row-level security is enabled and forced on every table, and tenant-facing services connect as a non-owner application role.
Tenant scoping is enforced by a restrictive policy, which is ANDed with every applicable permissive policy and grants no access by itself. Every role must also have a permissive policy for each command it may run, and matching SQL privileges; permissive policies must not be used to widen access past the tenant scope. On a mixed table a tenant credential may read rows where
tenant_idisplatformor equals its own claim, and may write only rows carrying its owntenant_id. Shared rows are writable only through a separate, role-scoped policy for a platform or pack-publishing role. Policy shape:The tenant-boundary policy is only a filter; a missing or empty
app.tenant_iddenies tenant access.read_scopeandwrite_ownare separate permissive policies, intentionally ORed for reads on the mixed table; the restrictive policy still limits both to the platform and current-tenant rows. SplitFOR ALLinto the specific commands each role may run, and add matching SQL privileges. A trusted publisher uses separate restrictive and permissive role-scoped policies limited totenant_id = 'platform'.app.tenant_idis set by the service from the authenticated credential’stenant_idclaim and never from request input (Tenancy Spec §4.3).agent_tool_call_recordsandagent_tool_call_resultsuse the same restrictive tenant filter and tenant-scopedSELECT/INSERTpolicies, with noUPDATEpolicy or grant. The tenant-scopedINSERTon results rejects the sweep-only status, and a separate role writes it:agent_invocation_outcomesuses the same restrictive tenant filter and tenant-scoped permissiveSELECT/INSERTpolicies asagent_invocations. It has noUPDATEpolicy or grant; the retention-role delete path is described below:On every tenant-facing table the restrictive tenant policy is evaluated alongside role-specific domain-pack and business-instance policies; the effective scope is their intersection. Missing tenant context denies tenant-facing access, including reads of shared rows.
Keys and indexes.
tenant_idleads every composite primary key, composite unique key and composite index on these tables, and every foreign key is backed by an index led bytenant_id.- A caller-chosen identifier, one whose value a client or an author picks (for example
agent_idorsuite_id), is never a single-column key. Mixed tables key it as(tenant_id, <id>), so two tenants can use the same value and a clash reveals nothing. - A server-generated surrogate (a
gen_random_uuid()column such asid,version_idormemory_id) may be a single-column primary key, because a generated value cannot clash across tenants and so reveals nothing by clashing. This holds only while the value is assigned by the database:DEFAULT gen_random_uuid()is skipped when an insert supplies the column. The database therefore overwrites any supplied value with aBEFORE INSERTtrigger (see Server-assigned keys). A surrogate without that trigger is treated as caller-chosen and keyed(tenant_id, <id>), asmemory_tombstones.memory_id, which carries the value one-to-one frommemory_records.memory_id, is. A table that other tables reference must also carryUNIQUE (tenant_id, <surrogate>), or a composite primary key, so that composite foreign keys can reference it. A table nothing references needs no such key, and a leaf table’s index on its surrogate is not covered by this rule. - A global unique key is permitted only where the table’s DDL says so and the value is server-generated.
agent_invocation_outcomes.outcome_idandworkflow_outcomes.outcome_idare the cases: each is globally unique per the Outcome Spec, and Neo4j keys onoutcome_idalone. Because the writer embeds it in the envelope, it is minted bymint_outcome_id()and checked by trigger rather than assigned by default (see Server-assigned keys).
A reference from one row to another in the same scope uses a composite foreign key
(tenant_id, id)to a matching key. Every child of a mixed table (versions, prompt layers, policies and the rest of Groups A, B, C and E) is in the same scope as its parent, so its composite foreign key forces the sametenant_id.A tenant-owned row, or a tenant’s workflow assignment, may reference a shared parent as well as its own. Such a row carries an explicit scope column (
agent_scope,assigned_agent_scope) constrained byCHECK (scope IN ('platform', tenant_id)), and its foreign key is(scope, id). A row therefore cannot reference another tenant’s parent, and no trigger is needed.An agent id owned by a tenant must differ from every shared agent id. The registration path enforces this. Shared ids are readable by every tenant, so rejecting a clash reveals nothing. Two tenants may use the same id, because the key is
(tenant_id, agent_id).business_instance_idis the inner scope key and is meaningful only with itstenant_id. The formerinstance_idcolumns are renamed to it.
Server-assigned keys
Rule 4 lets a single-column surrogate key stand only while no tenant credential can choose its value. Postgres skips DEFAULT gen_random_uuid() when an insert supplies the column, so the database must assign it itself. Apply this block after the tables exist.
These are the 29 tables whose primary key is a single gen_random_uuid() column. Tables keyed (tenant_id, <id>) need no trigger, because their identifier cannot clash across tenants. memory_tombstones is keyed (tenant_id, memory_id) for the same reason.
agent_invocation_outcomes.outcome_id and workflow_outcomes.outcome_id cannot be assigned by an insert default, because the writer embeds each in the Outcome Envelope and a CHECK (or insert-time check) requires the minted value to match. Each is instead minted first by a database function, and an insert trigger rejects any value not minted for the same tenant. The rejection message is the same for an unknown value and for one minted by another tenant, so a probe learns nothing about other tenants’ ids. The global UNIQUE (outcome_id) can then only ever collide with a value minted for this tenant.
Each minted outcome_id is single-claim: after a successful insert into either agent_invocation_outcomes or workflow_outcomes, the mint is marked consumed so the same UUID cannot be inserted into the other table (Neo4j keys Outcome nodes on outcome_id alone). Create these after both outcome tables exist:
The writer calls mint_outcome_id(<tenant_id>) when it creates the terminal envelope, puts the result in both the envelope and the column, and inserts. The DEFAULT gen_random_uuid() on the column is then unused by tenant writers and stays only as a fallback for trusted platform writers, which the trigger still checks. The same mint-and-check pattern applies to workflow_outcomes.outcome_id.
Classification
| Group | Tables | Class |
|---|---|---|
| A | agent_definitions, agent_versions, agent_prompt_layers, agent_authority_profiles, agent_contracts, agent_tool_bindings, agent_memory_profiles, agent_task_surfaces, agent_workflow_roles, agent_interop_profiles, agent_policy_bindings | Mixed |
| B | agent_registry_entries, agent_registry_versions, agent_capability_tags, agent_dependency_declarations, agent_lifecycle_events | Mixed |
| B | agent_instance_controls | Tenant-owned |
| C | agent_runtime_policies, agent_runtime_policy_versions, agent_model_routing_rules | Mixed |
| C | agent_circuit_breaker_state, agent_runtime_events | Tenant-owned |
| D | agent_invocations, agent_invocation_outcomes, workflow_outcomes, agent_tool_call_records, agent_tool_call_results | Tenant-owned |
| D | workflow_task_assignments | Mixed |
| E | agent_eval_suites, agent_eval_runs, agent_eval_results, agent_eval_promotion_gates | Mixed |
| E | agent_eval_compliance_events | Tenant-owned |
| F | agent_lineage_links | Tenant-owned |
| G | memory_records, memory_record_lineage, memory_promotions, memory_tombstones, memory_recall_log | Tenant-owned |
| H | organisations, users, role_assignments, authority_delegations, approval_decisions, role_assignment_events, service_principals | Tenant-owned |
The OS agent definitions os-agt-001 to os-agt-012, and the definitions that domain packs and Capability Modules contribute under the Agent Registry §6, are the shared rows (tenant_id = platform) of the mixed tables. A Business Instance Extension’s agents are tenant-owned rows. Groups A–G have DDL in this version; Group H normative DDL lives in Synedrix OS — Identity and Access Spec § Persistence and is inventoried here. Classes in the table above remain normative.
The circuit breaker is per tenant
agent_circuit_breaker_state was one row per agent. With pooling, one tenant’s failures would have opened the breaker for every tenant. It is now keyed (tenant_id, agent_id).
The task-graph tables
runtime.task_graphs, runtime.tasks, runtime.task_edges and runtime.executions are defined by the Task Graph Spec, not here. They are tenant-owned and follow these rules; that spec carries the normative SQL.
Where each group is hosted
This version makes every table safe to pool: each carries the tenant boundary whether it sits in the pooled control-plane Postgres or in a tenant’s own. It does not decide which. Groups D and G hold tenant content (task_input, task_output, request and response payloads, memory_records.content), and whether that content stays in the tenant’s data plane with only an index in the control plane is the open decision recorded in the Tenancy Spec §9.
How the table groups divide across the tenant boundary, the task-graph tables and the graph projection:
flowchart TB
subgraph mixed["Mixed: shared rows use tenant_id platform"]
GA["Group A: Agent Definition"]
GB["Group B: registry entries, versions, capability tags, dependency declarations, lifecycle events"]
GC["Group C: runtime policies, policy versions, model routing rules"]
GD1["Group D: workflow task assignments"]
GE["Group E: eval suites, runs, results, promotion gates"]
end
subgraph owned["Tenant-owned: tenant_id never platform"]
GB2["Group B: agent instance controls"]
GC2["Group C: circuit breaker state, runtime events"]
GD["Group D: invocations, outcomes, workflow outcomes, tool call records and results"]
GE2["Group E: eval compliance events"]
GG["Group G: Memory Control Plane"]
GH["Group H: Identity and Access"]
GF["Group F: Lineage"]
TG["Task-graph tables: runtime.task_graphs, tasks, task_edges, executions"]
end
TGS["Task Graph Spec: normative SQL"]
NEO["Neo4j: queryable graph projection"]
HOST{"Hosting of tenant content: control-plane Postgres or tenant data plane"}
OPEN["Open decision: Tenancy Spec section 9"]
TG --> TGS
GF -->|"synced to"| NEO
GD --> HOST
GG --> HOST
HOST --> OPEN
Group A — Agent Definition
agent_definitions
Primary identity record for each agent. One row per agent_id.
agent_versions
Full version history for each agent definition.
agent_prompt_layers
Versioned prompt layer assignments per agent. One row per layer type per agent version.
agent_authority_profiles
Authority boundary declaration per agent version.
agent_contracts
Input/output contract declarations per agent version.
agent_tool_bindings
Tool permission records per agent version. One row per tool.
agent_memory_profiles
Memory access scope declarations per agent version.
agent_task_surfaces
Task class declarations per agent version.
agent_workflow_roles
Binding of agent to specific workflow and role, referencing a task class.
agent_interop_profiles
MCP and A2A interoperability configuration per agent version.
agent_policy_bindings
Governance policy assignments per agent version.
Group B — Registry & Lifecycle
agent_registry_entries
Current live registry record per agent. Mirrors key fields from definition for fast discovery.
agent_registry_versions
Discovery-surface changelog history per agent. Distinct from agent_versions, which stores full definition snapshots. Each row records a SemVer change to the registry entry; definition_version may point at the agent_versions.version that became current.
A publish is one transaction: insert the agent_registry_versions row, then insert or update the agent_registry_entries row to the same version.
agent_capability_tags
Normalized capability tags for discovery. Complements agent_registry_entries.capability_tags (array retained for fast read). domain_tags and workflow_role_classes remain arrays on the registry entry in this version.
agent_dependency_declarations
Registry completeness surface for required tools, policy packs, memory layers, and workflows. Does not replace Group A bindings (agent_tool_bindings, agent_memory_profiles, agent_workflow_roles); those remain the authority ceilings.
agent_lifecycle_events
Immutable audit log of every lifecycle state transition.
agent_instance_controls
Business-instance-level enable/disable and allowed overrides.
Group C — Runtime Policy
agent_runtime_policies
Current active runtime policy per agent.
agent_runtime_policy_versions
Versioned full-policy snapshots. The current flattened row in agent_runtime_policies is the live pointer; this table stores reproducible policy_snapshot JSON for every version string an invocation may pin via agent_invocations.runtime_policy_version.
agent_model_routing_rules
Ordered custom routing rules for model_routing_strategy = custom-router-ref, and optional condition rows for other strategies when a ruleset is declared.
agent_circuit_breaker_state
Current circuit breaker state per agent.
agent_runtime_events
Append-only runtime coordination events. invocation_id is indexed but not a foreign key so indefinite retention of events cannot block invocation retention. Bus event names map to event_type as: agent.runtime.model-fallback → fallback, agent.runtime.degraded-mode → degradation, agent.runtime.escalated → escalation; retry and circuit-breaker transitions use retry and circuit_breaker_transition; conflicting terminal outcomes use outcome_conflict.
Group D — Execution Records (append-only)
agent_invocations
Immutable start record for every agent task execution. Insert it before the agent executes or invokes a tool. It contains only identity, immutable request context, and start-time fields; it is never updated. correlation_id groups related work and is not an idempotency key. Each start has a tenant-scoped idempotency_key so a retried start write resolves to the same invocation rather than creating another one. It also snapshots the governing limits: runtime_policy_version is the exact runtime policy version resolved at start, and deadline_at is resolved once, at start, as started_at plus the first available of: the task_timeout_ms of the invocation’s task_class in the task surface of the agent_version being run (it is nullable and is not a runtime-policy field); the runtime policy’s latency_budget_ms; the platform default invocation timeout. The start insert therefore always has a value, and latency_budget_ms is used only when the task surface declares no timeout. The recovery sweep reads these fields and never the current configuration, so a later change to the agent’s timeout or policy cannot make it time out a run too early or too late.
The caller generates a stable idempotency_key for one logical invocation attempt. On a duplicate key, the orchestrator reads and verifies the existing start row and resumes or returns its state; it MUST NOT replace the row or treat a shared correlation_id as unique.
agent_invocation_outcomes
Exactly one immutable terminal outcome per invocation. outcome_id is minted when the terminal outcome envelope is created; it does not exist on an in-flight invocation. The outcome row uses the canonical Outcome Spec statuses. outcome_envelope stores the complete canonical envelope, including policy, the full escalation object (target, path, triggering rule, hitl_request_id), quality, errors, source and timing, because the emitted os.outcome.* event omits them and the Audit and Lineage Agent must be able to reconstruct why and under whose authority an outcome occurred. The typed columns are query projections of the envelope and the invocation-level metrics defined by the Observability Metrics Spec; the envelope wins on any disagreement, and the writer derives both from the same object in one insert. outcome_id is unique across tenants as well as within one, which is why the second UNIQUE exists beside the composite key; a collision is a writer defect and fails the insert.
The current-state view gives readers the immutable start fields and optional terminal result without writing a running row into Redis or updating Postgres:
GRANT SELECT, INSERT ON agent_invocations, agent_invocation_outcomes, workflow_outcomes, agent_tool_call_records, agent_tool_call_results TO tenant_app_role;
GRANT SELECT ON agent_invocations_current TO tenant_app_role;The view is security-invoker so both base tables’ forced RLS policies remain effective. A row without an outcome is still in progress or orphaned; the recovery sweep identifies it once its own deadline_at plus a grace period has elapsed with no live execution heartbeat, and inserts a timeout outcome. Before doing so it closes any intent-only tool call of that invocation (see agent_tool_call_results). Because the outcome primary key makes the first terminal write final, a writer whose insert conflicts MUST read the existing outcome and escalate the discrepancy as an agent_runtime_events row rather than overwrite or discard it silently; the sweep MUST NOT write while the execution is still reporting liveness. Redis may hold ephemeral execution progress, but never invocation identity, tool-call audit rows, or a substitute for a Postgres write.
workflow_outcomes
Append-only terminal record for a workflow execution. Companion mirror of the normative DDL in Synedrix OS - Outcome Spec. outcome_id is minted with mint_outcome_id(<tenant_id>) and checked by the same insert trigger as agent outcomes; the table is tenant-owned with forced RLS and no tenant UPDATE/DELETE.
Minted outcome_id values are single-claim: the claim_minted_outcome_id() function and the two claim_* triggers are defined once, with the mint objects under Server-assigned keys above, and are created after both outcome tables exist.
agent_tool_call_records
Immutable intent record for each tool call within an invocation. A tool’s external side effect and a Postgres write cannot share a transaction, so the intent row is inserted and committed before the tool is dispatched; the call’s result is appended afterwards as a separate row in agent_tool_call_results. A crash between dispatch and result therefore leaves an intent row with no result, which marks a call whose side effect is unknown, not a missing audit record. Do not buffer either write in Redis. (tenant_id, invocation_id, hop_index) is unique, so a retried insert of the same call resolves to the existing row (ON CONFLICT DO NOTHING, then verify it matches) instead of duplicating the audit record; the caller reuses that row’s id as the idempotency token toward the tool where the tool supports one. A tool that does not support an idempotency token is never re-dispatched when its intent row already exists without a result: the retry marks the call unknown through the recovery path, escalates it for reconciliation, and does not repeat a write or irreversible side effect. Only a tool declared read-only, or one that accepts the token, may be re-dispatched after an ambiguous first attempt.
agent_tool_call_results
Exactly one immutable terminal result per tool-call intent, inserted when the call returns. The insert is replay-safe: use ON CONFLICT DO NOTHING, then read and verify the existing row, so a retry after a lost commit acknowledgement succeeds instead of failing on the primary key. unknown is written only by the recovery sweep (platform_recovery_role; tenant credentials cannot insert it, so a runtime cannot pre-empt the real result), for an intent row that still has no result when its invocation passes deadline_at plus the grace period; it records that dispatch may or may not have produced the side effect, and operators reconcile it against the target system.
workflow_task_assignments
Binding of a task class to an agent within a specific workflow definition. This is the registry-level declaration, separate from the live invocation record.
Group E — Evaluation & Quality
agent_eval_suites
Evaluation suite definitions per agent.
agent_eval_runs
Historical record of every suite execution.
agent_eval_results
Per-check results within an evaluation run. Distinct from Observability Metrics Spec evaluation_results (metric-family store); correlate via agent_id, run_id/trace_id, and timestamps — do not merge the tables.
agent_eval_promotion_gates
Gate outcome recorded per lifecycle transition attempt.
agent_eval_compliance_events
Immutable audit log of policy compliance breaches. A breach is never updated. Its resolution is recorded as a new row whose resolves_event_id references the breach it closes, with the resolution notes in description. A breach is open while no row resolves it. At most one row resolves a breach, and only a breach row can be resolved, never a resolution row. A resolution row copies breach_type and severity from the breach it closes, because both are required.
Group F — Lineage
agent_lineage_links
Synced to Neo4j as the authoritative relationship graph. Postgres holds the canonical write record; Neo4j holds the queryable graph projection.
Outcome-to-outcome causal edges MUST use source_type = target_type = 'outcome', source_id/target_id as outcome_id text, and one of invoked / contributed-to / escalated-to / governed-by. The Neo4j projection maps those four relation strings to backtick-escaped Cypher relationship types (Neo4j Implementation Guide). Structural edges (depends-on, replaced-by) may use agent/workflow/tool/policy endpoints and are not Outcome nodes.
Endpoint compatibility (normative): Outcome relations (invoked, contributed-to, escalated-to, governed-by) REQUIRE source_type = target_type = 'outcome'. Structural relations (depends-on, replaced-by) REQUIRE both endpoints in (agent, workflow, tool, policy). Enforce with:
Tenant-scoped endpoint resolution (normative). A tenant writer must not be able to name another tenant’s outcome id, because the Neo4j projection keys Outcome nodes on outcome_id alone and would connect the tenants. A BEFORE INSERT trigger therefore resolves every outcome endpoint in the row’s own tenant_id, and rejects an unknown or foreign id with one message so a probe learns nothing:
Group G — Memory Control Plane
Implements Memory Fabric Spec. Supabase PG17 is write-authoritative for every audit-bearing tier; the Qdrant collections and the Neo4j entity graph are projections that must be rebuildable from these tables without behaviour change.
memory_records
The governed unit of memory. One row per record, carrying the §4 envelope.
memory_record_lineage
The derived_from[] edge set, normalized. A consolidated assertion must be traceable to the episodes that
produced it, which is what makes a contested claim auditable back to its sources.
memory_promotions
Append-only. Every tier transition and every visibility widening, with the approvals that authorized it.
memory_tombstones
Append-only. Written when a record’s content is erased. The tombstone is the durable proof that a record existed, what produced it, and that it was destroyed under an approval — so an erasure never punches a hole in the audit trail.
memory_recall_log
Append-only. Every brokered recall decision, including denials. Written by os-agt-012, which never sees
record content — this table stores the decision and its scope, not what was returned.
Group H — Identity & Access
Inventory of the identity and access control-plane tables. Normative DDL lives in Synedrix OS — Identity and Access Spec § Persistence (Group H); this schema lists them so the Agent DB group inventory and Classification matrix stay complete. All seven tables are tenant-owned (tenant_id never platform). Secret values never appear here — only references such as service_principals.secret_ref.
| Table | Role |
|---|---|
organisations | Tenant root; tenant_id is the organisation id |
users | Human principals bound to an organisation / IdP subject |
role_assignments | Control-role or instance-role grants, scoped and time-bound |
authority_delegations | Time-bound approval-authority delegations within a tenant |
approval_decisions | Append-only HITL decision audit |
role_assignment_events | Append-only audit of every role grant and end (actor, time) |
service_principals | Non-human credential metadata and scopes |
Server-assigned UUID surrogates and forced RLS follow the Identity Spec Persistence block and the Tenant boundary rules above.
Retention policies
| Table group | Retention | Rationale |
|---|---|---|
| Agent definitions and versions | Indefinite | Canonical configuration truth; required for audit lineage. |
| Registry and lifecycle events | Indefinite | Every lifecycle change is a permanent audit record. |
| Runtime policies | Indefinite per version | Required for reproducing execution context at any past invocation. |
| Agent invocations | Configurable per compliance class; minimum 1 year for high-risk agents | Audit trail for all production executions. |
| Agent invocation outcomes | Same compliance class and retention as their invocation | Terminal result and metric fields are part of the invocation audit record. |
| Workflow outcomes | Configurable per compliance class; minimum 1 year for high-risk workflows | Terminal workflow audit record; companion to Outcome Spec v0.3. |
| Identity & access (Group H) | Indefinite for assignments, delegations and approval decisions; organisations/users follow offboarding and erasure policy | Control-plane identity audit; normative DDL in Identity Spec v0.3. |
| Tool call records and results | Configurable; minimum 90 days; a result has the same retention as its intent record | Debugging and compliance evidence. |
| Agent runtime events | Indefinite | Append-only retry, fallback, degradation, escalation, and circuit-breaker transition history. |
| Eval runs and results | Indefinite | Required for promotion gate lineage. |
| Compliance events | Indefinite | Permanent compliance and incident record. |
| Memory records | Per retention_class; audit never decays | Layer 7 owns the mechanism, Layer 12 sets the values. |
| Memory promotions, tombstones, recall log | Indefinite | Append-only governance record; tombstones and lineage must outlive the content they describe. |
Key design decisions
Why normalized tables instead of JSONB blobs for agent sub-objects?
JSONB blobs make individual field queries, joins, and index-based filtering expensive, and they make schema migrations implicit. Normalizing sub-objects into separate tables allows each concern (authority, memory profile, tool bindings) to evolve independently without touching the parent record, and allows RLS policies to be applied granularly.
Why are invocation records append-only?
Invocations are part of the audit trail. Synedrix OS requires that every action be traceable with a clear what, why, and under whose authority. A mutable invocation record cannot satisfy this requirement reliably.
Why is workflow_task_assignments separate from agent_workflow_roles?
agent_workflow_roles is part of the agent definition and declares the agent’s capability surface. workflow_task_assignments is part of the workflow registry and declares who does what in a specific workflow version. The same agent can play a role in multiple workflows; the same workflow can change which agent it assigns to a task class across versions. Keeping these separate prevents tight coupling between agent versioning and workflow versioning.
Invariants
- All canonical agent configuration lives in Supabase Postgres. Redis holds only ephemeral hot state.
- Invocation starts, terminal outcomes, tool-call records, lifecycle events, and compliance breach events are append-only and must never be updated. Only the retention role may delete eligible invocation starts, outcomes, tool-call records, and tool-call results under policy.
memory_record_lineagerows are immutable;memory_promotions,memory_tombstonesandmemory_recall_logare append-only. - An invocation start row is inserted before execution; each tool call’s intent is inserted before dispatch and its result immediately after it returns; exactly one terminal outcome row is inserted per invocation. Redis is not a persistence or recovery buffer for these records.
- Each agent-level retry is a new
agent_runtime_eventsrow linked to the existing invocation. At minimum that table carriestenant_id,event_id,invocation_id(an indexed reference led bytenant_id, deliberately not a foreign key: runtime events are retained indefinitely and must not block retention of the invocation they describe),event_type(retry,fallback,degradation,escalation,circuit_breaker_transition,outcome_conflict),attempt,detailsandoccurred_at; its DDL is defined with the other tables listed as having none.retry_countin its outcome is a terminal summary, not a mutable counter.agent_circuit_breaker_stateremains a mutable current-state projection; every state transition is also recorded inagent_runtime_events. - All tables must have forced RLS policies enforcing tenant, domain-pack and business-instance scoping.
- Every table carries a non-null
tenant_id. On a mixed table the reserved valueplatformmarks only shared reference rows, which tenant credentials cannot write; on a tenant-owned tableplatformis never a valid value. - Lineage links are written to Postgres first, then synced to Neo4j. Postgres is the write-authoritative source.
- No domain pack may add columns to OS-owned tables. Extension must go through the spec versioning process.
memory_recordsis write-authoritative for every audit-bearing tier. Qdrant collections and the Neo4j entity graph are projections and must be rebuildable from it without behaviour change.memory_promotions,memory_tombstonesandmemory_recall_logare append-only.- Only
os-agt-010writesmemory_recordsrows in the episodic, semantic, procedural or profile tiers. - Erasure sets
contentto NULL and writes amemory_tombstonesrow in the same transaction.content_hash, provenance andmemory_record_lineagerows are never deleted.
Every append-only or immutable table must also be protected at the privilege and RLS layers. Revoke UPDATE from PUBLIC and every application role, and revoke DELETE from PUBLIC and every application role that must not delete; the only delete exception is the policy-scoped retention role on invocation starts, outcomes, and tool-call records. Add restrictive policies that deny unauthorized commands. For each listed table, for example:
no_update names no role, so it applies to every role without BYPASSRLS, the retention role included. no_delete names the roles that must never delete, which leaves one path for it:
Retention role.
platform_retention_roledeletes through its own permissive policies, because with forced row-level security a role withDELETEand no applicable permissive policy deletes nothing. It hasDELETEand noUPDATE, is not named inno_delete, is not bound by the tenant-facingtenant_scopepolicy, and is never reachable from a tenant credential, like the bypass role in the Tenancy Spec. Each run writes an audit record.The outcome table uses the same
REVOKE UPDATE, DELETE,no_update, and tenant-roleno_deleteprotection. Grant the retention roleDELETEand add permissive policies for expiry usingcompleted_atand for offboarding by tenant:workflow_outcomesfollows the same pattern (itsREVOKEand tenant-roleno_deleteare in its DDL above), expiring oncreated_at(it has no invocation parent, so it expires independently on its own compliance class):Apply the same grant and policies to
agent_tool_call_recordsusingcreated_at, and toagent_tool_call_resultsusingcompleted_at. The outcome and tool-call intent rows referenceagent_invocations, and results reference intent rows; retention deletes in this order, in one transaction: outcomes, tool-call results, tool-call intent rows, then the invocation. Eligibility is decided once per invocation fromagent_invocations.started_atand its compliance class, and applied to the start row, its outcome and its tool-call rows together (the per-tablecompleted_atandcreated_atpredicates above are only the row-level guards). The retention job must skip an invocation unless its outcome and every dependent tool-call row are eligible under the same compliance-class retention decision; it must also skip an invocation still referenced by an indefinitely retained compliance event, because the foreign key would block deletion.agent_runtime_eventscarries no foreign key to the invocation, so its indefinitely retained rows never block this delete; they keep the identifiers after the invocation rows expire. The retention interval is supplied by the job from the Retention policies table per compliance class.Never deleted by the retention role.
agent_lifecycle_events,agent_eval_compliance_eventsandmemory_record_lineage(retained indefinitely or immutable), andmemory_promotions,memory_tombstonesandmemory_recall_log, are not granted to it. Offboarding therefore removes only the tenant’s eligible invocation starts, outcomes, tool-call records, and tool-call results; the tables above stay until the erasure question in Open items is decided. Erasure of memory content is a tombstone, not a delete.Add any new application role to the
REVOKEand tono_delete’s role list, unless it is explicitly granted the scoped retention exception on the three eligible tables, so a role cannot gain either command by omission.
Apply the same revocation and restrictive policies to agent_invocation_outcomes, agent_tool_call_records, agent_tool_call_results, agent_runtime_events, agent_lifecycle_events, agent_eval_compliance_events, memory_record_lineage, memory_promotions, memory_tombstones and memory_recall_log, naming every other application role in the revocation as well. No role can update these records, and no tenant-facing role can delete them, even if another policy is added; only the retention role deletes from agent_invocations, agent_invocation_outcomes, workflow_outcomes, agent_tool_call_records and agent_tool_call_results, as above. agent_circuit_breaker_state is not an audit log: it remains mutable and its history is reconstructed from runtime events.
Open items
- Offboarding and erasure of indefinitely retained tables. The retention role deletes
agent_invocations,agent_invocation_outcomes,workflow_outcomes,agent_tool_call_recordsandagent_tool_call_results. Whether a tenant leaving, or a right-to-erasure request, must also remove that tenant’s lifecycle events, compliance events and the memory promotion, tombstone and recall-log rows is not decided here. It needs a decision from the owner of the retention and erasure policy, and the Memory Fabric Spec erasure rule constrains the memory tables. - Hosting of Groups D and G. Whether tenant content stays in each tenant’s own Postgres, with only an index in the pooled control plane, or sits in the pooled one under row-level security. This version is safe either way. See the Tenancy Spec §9.
- Tenant agent ids versus shared ids (Tenant boundary rule 7). Enforced by the registration path, not by a key. A constraint that makes the clash impossible at the table level is not chosen.
- The
runtime.*tables. Normative SQL lives in Task Graph Spec v0.3. The Goal Model Spec v0.3 carriestenant_idon a Goal Instance. - Migration of existing data. No migration is specified because the schema has not been deployed; a deployment before this version requires an explicit migration plan.
Versioning
- Version: 1.4
- Supersedes: v1.3
- Related: Synedrix OS - Agent Spec, Synedrix OS Memory Fabric Spec, Synedrix OS — Identity and Access Spec, Synedrix OS - Outcome Spec, Synedrix OS - Agent Registry Spec, Synedrix OS - Agent Runtime Policy Spec, Synedrix OS - Agent Evaluation Spec, Synedrix OS Tech Stack, Synedrix OS Observability Metrics Spec
Related Documents
Agent Specifications
- Synedrix OS - Agent Spec — Full agent anatomy; the normative field schema here maps directly to the database tables defined in this spec.
- Synedrix OS - Agent Registry Spec — Registry schema; the agent_registry_entries table group implements this spec.
- Synedrix OS - Agent Runtime Policy Spec — Runtime policy schema; the runtime policy table group implements this spec.
- Synedrix OS - Agent Evaluation Spec — Evaluation spec; the evaluation/quality table group implements this spec.
Tech Stack
- Synedrix OS Tech Stack — 12-platform stack; Supabase PG17 is the canonical relational store for all tables in this schema.
Tenancy
- Tenancy Spec — The tenant, plane boundary and scoping rules this document implements.
- ADR-OS-008 — Tenancy Model — The decision behind the tenant boundary.
Identity, outcomes and architecture
- Synedrix OS — Identity and Access Spec — Normative Group H DDL (organisations, users, roles, delegations, approvals, service principals).
- Synedrix OS - Outcome Spec — Outcome envelope; normative owner of
workflow_outcomesDDL mirrored under Group D. - Synedrix OS Event Taxonomy Standard — Event standard; agent lifecycle events stored in the lineage table group conform to the shared event envelope.
- Synedrix OS Observability Metrics Spec — Observability metrics spec; defines the metric fields stored on
agent_invocation_outcomesand five related observability tables. - Synedrix OS - Consolidated Canonical Reference — Consolidated synthesis index.
Synedrix OS · April 2026