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)
Illustrative example, not a production implementation. Review and test before use.

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

ClassMeaningtenant_id
ReferenceDefinitions shared by every tenant. Read-only to tenant credentials, writable only by a platform or pack-publishing roleAbsent
MixedA table holding both shared definitions and tenant-contributed ones, such as the agents a Business Instance Extension registersNon-null. The reserved value platform marks a shared row; any other value is the owning tenant
Tenant-ownedRecords that belong to one tenantNon-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

  1. Row-level security is enabled and forced on every table, and tenant-facing services connect as a non-owner application role.

  2. 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_id is platform or equals its own claim, and may write only rows carrying its own tenant_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_id denies tenant access. read_scope and write_own are separate permissive policies, intentionally ORed for reads on the mixed table; the restrictive policy still limits both to the platform and current-tenant rows. Split FOR ALL into the specific commands each role may run, and add matching SQL privileges. A trusted publisher uses separate restrictive and permissive role-scoped policies limited to tenant_id = 'platform'. app.tenant_id is set by the service from the authenticated credential’s tenant_id claim and never from request input (Tenancy Spec §4.3).

    agent_tool_call_records and agent_tool_call_results use the same restrictive tenant filter and tenant-scoped SELECT/INSERT policies, with no UPDATE policy or grant. The tenant-scoped INSERT on results rejects the sweep-only status, and a separate role writes it:

    agent_invocation_outcomes uses the same restrictive tenant filter and tenant-scoped permissive SELECT/INSERT policies as agent_invocations. It has no UPDATE policy or grant; the retention-role delete path is described below:

  3. 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.

  4. Keys and indexes.

    • tenant_id leads every composite primary key, composite unique key and composite index on these tables, and every foreign key is backed by an index led by tenant_id.
    • A caller-chosen identifier, one whose value a client or an author picks (for example agent_id or suite_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 as id, version_id or memory_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 a BEFORE INSERT trigger (see Server-assigned keys). A surrogate without that trigger is treated as caller-chosen and keyed (tenant_id, <id>), as memory_tombstones.memory_id, which carries the value one-to-one from memory_records.memory_id, is. A table that other tables reference must also carry UNIQUE (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_id and workflow_outcomes.outcome_id are the cases: each is globally unique per the Outcome Spec, and Neo4j keys on outcome_id alone. Because the writer embeds it in the envelope, it is minted by mint_outcome_id() and checked by trigger rather than assigned by default (see Server-assigned keys).
  5. 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 same tenant_id.

  6. 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 by CHECK (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.

  7. 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).

  8. business_instance_id is the inner scope key and is meaningful only with its tenant_id. The former instance_id columns 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

GroupTablesClass
Aagent_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_bindingsMixed
Bagent_registry_entries, agent_registry_versions, agent_capability_tags, agent_dependency_declarations, agent_lifecycle_eventsMixed
Bagent_instance_controlsTenant-owned
Cagent_runtime_policies, agent_runtime_policy_versions, agent_model_routing_rulesMixed
Cagent_circuit_breaker_state, agent_runtime_eventsTenant-owned
Dagent_invocations, agent_invocation_outcomes, workflow_outcomes, agent_tool_call_records, agent_tool_call_resultsTenant-owned
Dworkflow_task_assignmentsMixed
Eagent_eval_suites, agent_eval_runs, agent_eval_results, agent_eval_promotion_gatesMixed
Eagent_eval_compliance_eventsTenant-owned
Fagent_lineage_linksTenant-owned
Gmemory_records, memory_record_lineage, memory_promotions, memory_tombstones, memory_recall_logTenant-owned
Horganisations, users, role_assignments, authority_delegations, approval_decisions, role_assignment_events, service_principalsTenant-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;
Illustrative example, not a production implementation. Review and test before use.

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

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.

TableRole
organisationsTenant root; tenant_id is the organisation id
usersHuman principals bound to an organisation / IdP subject
role_assignmentsControl-role or instance-role grants, scoped and time-bound
authority_delegationsTime-bound approval-authority delegations within a tenant
approval_decisionsAppend-only HITL decision audit
role_assignment_eventsAppend-only audit of every role grant and end (actor, time)
service_principalsNon-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 groupRetentionRationale
Agent definitions and versionsIndefiniteCanonical configuration truth; required for audit lineage.
Registry and lifecycle eventsIndefiniteEvery lifecycle change is a permanent audit record.
Runtime policiesIndefinite per versionRequired for reproducing execution context at any past invocation.
Agent invocationsConfigurable per compliance class; minimum 1 year for high-risk agentsAudit trail for all production executions.
Agent invocation outcomesSame compliance class and retention as their invocationTerminal result and metric fields are part of the invocation audit record.
Workflow outcomesConfigurable per compliance class; minimum 1 year for high-risk workflowsTerminal 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 policyControl-plane identity audit; normative DDL in Identity Spec v0.3.
Tool call records and resultsConfigurable; minimum 90 days; a result has the same retention as its intent recordDebugging and compliance evidence.
Agent runtime eventsIndefiniteAppend-only retry, fallback, degradation, escalation, and circuit-breaker transition history.
Eval runs and resultsIndefiniteRequired for promotion gate lineage.
Compliance eventsIndefinitePermanent compliance and incident record.
Memory recordsPer retention_class; audit never decaysLayer 7 owns the mechanism, Layer 12 sets the values.
Memory promotions, tombstones, recall logIndefiniteAppend-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_lineage rows are immutable; memory_promotions, memory_tombstones and memory_recall_log are 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_events row linked to the existing invocation. At minimum that table carries tenant_id, event_id, invocation_id (an indexed reference led by tenant_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, details and occurred_at; its DDL is defined with the other tables listed as having none. retry_count in its outcome is a terminal summary, not a mutable counter. agent_circuit_breaker_state remains a mutable current-state projection; every state transition is also recorded in agent_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 value platform marks only shared reference rows, which tenant credentials cannot write; on a tenant-owned table platform is 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_records is 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_tombstones and memory_recall_log are append-only.
  • Only os-agt-010 writes memory_records rows in the episodic, semantic, procedural or profile tiers.
  • Erasure sets content to NULL and writes a memory_tombstones row in the same transaction. content_hash, provenance and memory_record_lineage rows 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_role deletes through its own permissive policies, because with forced row-level security a role with DELETE and no applicable permissive policy deletes nothing. It has DELETE and no UPDATE, is not named in no_delete, is not bound by the tenant-facing tenant_scope policy, 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-role no_delete protection. Grant the retention role DELETE and add permissive policies for expiry using completed_at and for offboarding by tenant:

    workflow_outcomes follows the same pattern (its REVOKE and tenant-role no_delete are in its DDL above), expiring on created_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_records using created_at, and to agent_tool_call_results using completed_at. The outcome and tool-call intent rows reference agent_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 from agent_invocations.started_at and its compliance class, and applied to the start row, its outcome and its tool-call rows together (the per-table completed_at and created_at predicates 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_events carries 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_events and memory_record_lineage (retained indefinitely or immutable), and memory_promotions, memory_tombstones and memory_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 REVOKE and to no_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_records and agent_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 carries tenant_id on 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


Agent Specifications

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 · April 2026